यदि स्ट्रिंग्स .NET में अपरिवर्तनीय हैं, तो सबस्ट्रिंग O (n) समय क्यों लेता है?


451

यह देखते हुए कि स्ट्रिंग्स .NET में अपरिवर्तनीय हैं, मैं सोच रहा हूं कि उन्हें ऐसे क्यों डिज़ाइन किया गया है जो इसके बजाय string.Substring()O ( substring.Length) समय लेता हैO(1) ?

यानी ट्रेडऑफ क्या थे, यदि कोई हो


3
@ मेहरदाद: मुझे यह सवाल पसंद है। क्या आप मुझे बता सकते हैं कि हम .net में दिए गए फ़ंक्शन का O () कैसे निर्धारित कर सकते हैं? क्या यह स्पष्ट है या हमें इसकी गणना करनी चाहिए? धन्यवाद
odiseh

1
@odiseh: कभी-कभी (जैसे इस मामले में) यह स्पष्ट है कि स्ट्रिंग की प्रतिलिपि बनाई जा रही है। यदि ऐसा नहीं है, तो आप या तो प्रलेखन में देख सकते हैं, बेंचमार्क कर सकते हैं, या .NET फ्रेमवर्क स्रोत कोड में देखने का प्रयास कर सकते हैं कि यह क्या है।
user541686

जवाबों:


423

अद्यतन: मुझे यह सवाल बहुत पसंद आया, मैंने इसे अभी-अभी ब्लॉग किया है। स्ट्रिंग्स, अपरिवर्तनशीलता और दृढ़ता देखें


संक्षिप्त उत्तर है: O (n) O (1) है यदि n बड़ा नहीं होता है। अधिकांश लोग छोटे तारों से छोटे पदार्थों को निकालते हैं, इसलिए जटिलता कैसे बढ़ती है यह पूरी तरह से अप्रासंगिक है

लंबा उत्तर है:

एक अपरिवर्तनीय डेटा संरचना इस तरह से निर्मित होती है कि उदाहरण पर परिचालन केवल एक छोटी राशि (आमतौर पर O (1) या O (lg n)) की प्रतिलिपि के साथ मूल की मेमोरी के पुनः उपयोग की अनुमति देता है या नए आवंटन को "लगातार" कहा जाता है अपरिवर्तनीय डेटा संरचना। .NET में स्ट्रिंग्स अपरिवर्तनीय हैं; आपका प्रश्न अनिवार्य रूप से "वे लगातार क्यों नहीं हैं"?

क्योंकि जब आप उन ऑपरेशनों को देखते हैं जो आम तौर पर .NET प्रोग्राम्स में स्ट्रिंग्स पर किए जाते हैं , तो यह हर प्रासंगिक तरीके से होता है, जो कि पूरी तरह से नया स्ट्रिंग बनाने के लिए बिल्कुल भी बदतर नहीं होता हैएक जटिल सतत डेटा संरचना के निर्माण की लागत और कठिनाई खुद के लिए भुगतान नहीं करती है।

लोग आमतौर पर "स्ट्रिंग" का उपयोग एक छोटी स्ट्रिंग निकालने के लिए करते हैं - कहते हैं, दस या बीस अक्षर - कुछ हद तक स्ट्रिंग से - शायद कुछ सौ वर्ण। आपके पास अल्पविराम-अलग फ़ाइल में पाठ की एक पंक्ति है और आप तीसरे फ़ील्ड को निकालना चाहते हैं, जो एक अंतिम नाम है। लाइन शायद एक दो सौ वर्णों वाली होगी, नाम एक दर्जन होगा। स्ट्रिंग आवंटन और मेमोरी की पचास बाइट्स की नकल आधुनिक हार्डवेयर पर आश्चर्यजनक रूप से तेज है । यह एक नया डेटा संरचना बनाता है जिसमें एक मौजूदा स्ट्रिंग के बीच में एक पॉइंटर होता है और साथ ही एक लंबाई भी आश्चर्यजनक रूप से तेजी से अप्रासंगिक होती है; "फास्ट पर्याप्त" काफी तेजी से परिभाषा है।

निकाले गए पदार्थ आम तौर पर आकार में छोटे और जीवनकाल में छोटे होते हैं; कचरा संग्रहकर्ता जल्द ही उन्हें पुनः प्राप्त करने जा रहा है, और उन्होंने पहले स्थान पर ढेर पर ज्यादा जगह नहीं ली। इसलिए एक दृढ़ रणनीति का उपयोग करना जो अधिकांश स्मृति के पुन: उपयोग को प्रोत्साहित करता है, एक जीत भी नहीं है; आपके द्वारा किए गए सभी कामों से आपका कचरा संग्रहकर्ता धीमा हो जाता है क्योंकि अब इसे आंतरिक बिंदुओं को संभालने के बारे में चिंता करना पड़ता है।

यदि स्ट्रिंग करने वाले लोग आमतौर पर स्ट्रिंग्स पर काम करते हैं, तो वे पूरी तरह से अलग होते हैं, तो यह लगातार दृष्टिकोण के साथ जाने के लिए समझ में आता है। यदि लोगों के पास आम तौर पर मिलियन-कैरेक्टर स्ट्रिंग्स होते थे, और हजारों-हजार ओवरलैपिंग सब्सट्रिंग को आकार में सौ-हज़ार-कैरेक्टर रेंज में निकालते थे, और वे सबस्ट्रिंग ढेर पर लंबे समय तक रहते थे, तो यह लगातार समझ के साथ जाने के लिए सही समझ में आता है दृष्टिकोण; यह बेकार और मूर्खतापूर्ण होगा। लेकिन अधिकांश लाइन-ऑफ-बिजनेस प्रोग्रामर उन चीजों की तरह अस्पष्ट रूप से भी कुछ नहीं करते हैं। .NET एक ऐसा प्लेटफ़ॉर्म नहीं है जो मानव जीनोम प्रोजेक्ट की जरूरतों के अनुसार बनाया गया हो; डीएनए विश्लेषण प्रोग्रामर को हर दिन उन स्ट्रिंग उपयोग विशेषताओं के साथ समस्याओं को हल करना होगा; ऑड्स अच्छे हैं कि आप नहीं करते हैं। जो अपने स्वयं के लगातार डेटा संरचनाओं का निर्माण करते हैं जो उनके उपयोग परिदृश्यों के साथ निकटता से मेल खाते हैं।

उदाहरण के लिए, मेरी टीम ऐसे प्रोग्राम लिखती है जो आपके टाइप करते ही C # और VB कोड का विश्लेषण करते हैं। उन कोड फ़ाइलों में से कुछ बहुत बड़ी हैं और इस प्रकार हम सबस्ट्रिंग निकालने या वर्णों को निकालने या हटाने के लिए O (n) स्ट्रिंग हेरफेर नहीं कर सकते हैं। हम एक पाठ बफर जल्दी से और कुशलता मौजूदा स्ट्रिंग डेटा के थोक का फिर से उपयोग करने की अनुमति है कि करने के लिए संपादन प्रतिनिधित्व करने के लिए लगातार अपरिवर्तनीय डेटा संरचनाओं के एक समूह का निर्माण किया है और एक ठेठ संपादन पर मौजूदा शाब्दिक और वाक्यात्मक विश्लेषण। यह हल करने के लिए एक कठिन समस्या थी और इसका समाधान सी # और वीबी कोड संपादन के विशिष्ट डोमेन के अनुरूप था। हमारे लिए इस समस्या को हल करने के लिए अंतर्निहित स्ट्रिंग प्रकार की अपेक्षा करना अवास्तविक होगा।


47
यह विपरीत करना दिलचस्प होगा कि जावा कैसे करता है (या अतीत में किसी बिंदु पर कम से कम किया था) यह: एस्ट्रिंग एक नया स्ट्रिंग लौटाता है, लेकिन उसी चार की ओर इशारा करता है [] जितना बड़ा स्ट्रिंग - इसका मतलब है कि बड़ा चार [] अब तक कूड़े को एकत्र नहीं किया जा सकता है, जब तक कि सबस्टेशन दायरे से बाहर न हो जाए। मैं पसंद करता हूं। नेट का कार्यान्वयन दूर तक।
माइकल स्टम

13
मैंने इस तरह के कोड को बहुत कम देखा है: string contents = File.ReadAllText(filename); foreach (string line in content.Split("\n")) ...या इसके अन्य संस्करण। मेरा मतलब है कि एक पूरी फ़ाइल पढ़ें, फिर विभिन्न भागों की प्रक्रिया करें। उस प्रकार का कोड काफी तेज होगा और यदि स्ट्रिंग लगातार थी तो कम मेमोरी की आवश्यकता होती है; आपके पास प्रत्येक पंक्ति की प्रतिलिपि बनाने के बजाय हमेशा फ़ाइल की एक प्रति मेमोरी में होगी, फिर प्रत्येक पंक्ति के भाग आपकी प्रक्रिया के रूप में। हालांकि, जैसे एरिक ने कहा - यह विशिष्ट उपयोग का मामला नहीं है।
विन्यासकर्ता

18
@configurator: इसके अलावा, .NET 4 में File.ReadLines मेथड आपके लिए लाइनों में एक टेक्स्ट फाइल को तोड़ता है, यह सब पहले मेमोरी में पढ़े बिना।
एरिक लिपर्ट

8
@ मिचेल: जावा Stringको एक सतत डेटा संरचना के रूप में लागू किया जाता है (यह मानकों में निर्दिष्ट नहीं है, लेकिन सभी कार्यान्वयन मुझे पता है)।
जोआचिम सॉर

33
संक्षिप्त उत्तर: मूल स्ट्रिंग के कचरा संग्रह की अनुमति देने के लिए डेटा की एक प्रति बनाई जाती है
19ax में Qtax

121

सटीक रूप से क्योंकि स्ट्रिंग्स अपरिवर्तनीय हैं, .Substringउन्हें मूल स्ट्रिंग के कम से कम एक हिस्से की प्रतिलिपि बनाना होगा। N बाइट्स की एक प्रति बनाते समय O (n) समय लेना चाहिए।

आपको कैसे लगता है कि आप निरंतर समय में बाइट्स का एक गुच्छा कॉपी करेंगे ?


EDIT: मेहरदाद सुझाव देता है कि स्ट्रिंग को बिल्कुल भी कॉपी न करें, लेकिन इसके एक टुकड़े का संदर्भ रखते हुए।

.Net, एक बहु-मेगाबाइट स्ट्रिंग पर विचार करें, जिस पर कोई कॉल करता है .SubString(n, n+3)(स्ट्रिंग के बीच में किसी भी n के लिए)।

अब, एनटीआईआरई स्ट्रिंग को गारबेज कलेक्ट नहीं किया जा सकता है क्योंकि एक संदर्भ 4 वर्णों पर टिका है? यह अंतरिक्ष की एक हास्यास्पद बर्बादी की तरह लगता है।

इसके अलावा, सब्सट्रिंग (जो कि सब्सट्रिंग के अंदर भी हो सकता है) के संदर्भ को ट्रैक करता है, और GC को पराजित करने से बचने के लिए इष्टतम समय पर कॉपी करने की कोशिश करता है (जैसा कि ऊपर वर्णित है), अवधारणा को एक बुरा सपना बनाता है। यह बहुत सरल है, और अधिक विश्वसनीय है, जिस पर नकल करना .SubString, और सीधे अपरिवर्तनीय मॉडल को बनाए रखना है।


संपादित करें: यहाँ बड़े स्ट्रिंग्स के भीतर सबस्ट्रिंग के संदर्भों को रखने के खतरे के बारे में अच्छा पढ़ा गया है।


5
+1: बिल्कुल मेरे विचार। आंतरिक रूप से यह संभवतः उपयोग करता है memcpyजो अभी भी O (n) है।
लेप्पी जू

7
@abenky: मुझे लगता है कि शायद इसे बिल्कुल कॉपी न करके? यह पहले से ही है, आपको इसे कॉपी क्यों करना चाहिए?
user541686

2
@ मेहरदाद: यदि आप प्रदर्शन के बाद हैं। बस इस मामले में असुरक्षित जाओ। तब आप एक विकल्प प्राप्त कर सकते हैं char*
लेप्पी जू

9
@Mehrdad - आप बहुत ज्यादा वहाँ की उम्मीद कर रहे होंगे, यह कहा जाता है StringBuilder , और यह अच्छा के एक इमारत तार। इसे StringMultiPurposeManipulator नहीं कहा जाता है
MattDavey

3
@SamuelNeff, @Mehrdad: .NET में स्ट्रिंग्स समाप्त नहीं हुए हैं NULL। जैसा कि लिपर्ट के पोस्ट में बताया गया है , पहले 4 बाइट्स में स्ट्रिंग की लंबाई होती है। इसीलिए, जैसा कि स्कीट बताते हैं, वे \0वर्ण शामिल कर सकते हैं ।
एलीडब

33

जावा (जैसा कि .NET के विपरीत) करने के दो तरीके प्रदान करता है Substring(), आप इस पर विचार कर सकते हैं कि क्या आप केवल एक संदर्भ रखना चाहते हैं या किसी नए मेमोरी लोकेशन में पूरे विकल्प को कॉपी कर सकते हैं।

साधारण .substring(...)शेयरों में charमूल स्ट्रिंग ऑब्जेक्ट के साथ आंतरिक रूप से उपयोग की जाने वाली सरणी है, जिसे आप तब new String(...)एक नए सरणी में कॉपी कर सकते हैं, यदि आवश्यक हो (मूल एक के कचरा संग्रह में बाधा से बचने के लिए)।

मुझे लगता है कि इस तरह का लचीलापन एक डेवलपर के लिए सबसे अच्छा विकल्प है।


50
आप इसे "लचीलापन" कहते हैं, मैं इसे कॉल करता हूं "गलती से बग (या एक प्रदर्शन समस्या) का निदान करने के लिए सॉफ़्टवेयर में एक मुश्किल डालें क्योंकि मुझे नहीं पता था कि मुझे रोकना होगा और सभी जगहों के बारे में सोचना होगा कि यह कोड संभवतः हो सकता है। कहा जाता है (उनमें से जो केवल अगले संस्करण में आविष्कार किया जाएगा) बस एक स्ट्रिंग के बीच से 4 वर्ण प्राप्त करने के लिए "
नीर

3
डाउनवोट पीछे हट गया ... कोड के थोड़ा और अधिक सावधान ब्राउज़िंग के बाद यह जावा में एक विकल्प की तरह दिखता है एक साझा सरणी को संदर्भित करता है, कम से कम ओपनजेडक संस्करण में। और अगर आप एक नया तार सुनिश्चित करना चाहते हैं तो ऐसा करने का एक तरीका है।
डॉन रॉबी

11
@ नीर: मैं इसे "यथास्थिति पूर्वाग्रह" कहता हूं। आपके लिए ऐसा करने का जावा तरीका जोखिमों से भरा हुआ लगता है और .नेट ही एकमात्र सेंसबिल विकल्प है। जावा प्रोग्रामर के लिए, विपरीत मामला है।
माइकल बोर्गवर्ड्ट

7
मैं दृढ़ता से .NET को प्राथमिकता देता हूं, लेकिन यह लगता है कि एक चीज जावा सही है। यह उपयोगी है कि एक डेवलपर को सही मायने में O (1) सबस्ट्रिंग मेथड को एक्सेस करने की अनुमति दी जाए (बिना अपने स्वयं के स्ट्रिंग प्रकार को रोल किए, जो हर दूसरे लाइब्रेरी के साथ इंटरऑपरेबिलिटी में बाधा उत्पन्न करेगा, और एक अंतर्निहित समाधान के रूप में कुशल नहीं होगा। )। जावा का समाधान संभवतः अक्षम्य है (कम से कम दो ढेर वस्तुओं की आवश्यकता है, एक मूल स्ट्रिंग के लिए और दूसरा प्रतिस्थापन के लिए); स्लाइस का समर्थन करने वाली भाषाएँ स्टैक पर पॉइंटर्स की एक जोड़ी के साथ दूसरी वस्तु को प्रभावी ढंग से प्रतिस्थापित करती हैं।
क्वर्टी

10
JDK 7u6 के बाद से यह अब सच नहीं है - अब जावा हमेशा प्रत्येक के लिए स्ट्रिंग सामग्री की प्रतिलिपि बनाता है .substring(...)
Xaerxess

12

जावा बड़े तार का संदर्भ देता था, लेकिन:

जावा ने लीकिंग मेमोरी से बचने के लिए अपने व्यवहार को भी कॉपी करने के लिए बदल दिया

मुझे लगता है कि हालांकि इसमें सुधार किया जा सकता है: सिर्फ सशर्त रूप से नकल क्यों नहीं?

यदि विकल्प अभिभावक के कम से कम आधे आकार का है, तो कोई अभिभावक को संदर्भित कर सकता है। अन्यथा कोई सिर्फ एक प्रति बना सकता है। यह एक महत्वपूर्ण लाभ प्रदान करते हुए बहुत सी मेमोरी को लीक करने से बचाती है।


हमेशा नकल आपको आंतरिक सरणी को हटाने की अनुमति देती है। छोटे तार के सामान्य मामले में स्मृति को बचाने, ढेर आवंटन की संख्या को बढ़ाता है। इसका मतलब यह भी है कि आपको प्रत्येक वर्ण अभिगम के लिए अतिरिक्त अप्रत्यक्ष रूप से कूदने की आवश्यकता नहीं है।
कोडइन्चौस

2
मुझे लगता है कि इससे महत्वपूर्ण बात यह है कि जावा वास्तव में char[]एक नया बनाने के लिए एक ही आधार (अलग-अलग बिंदुओं के साथ शुरू और अंत) का उपयोग करने से बदल गया है String। यह स्पष्ट रूप से दिखाता है कि लागत-लाभ विश्लेषण को नए के निर्माण के लिए प्राथमिकता दिखानी चाहिए String
फिजलिसिस

2

यहां कोई भी उत्तर "ब्रैकेटिंग समस्या" को संबोधित नहीं करता है, जो यह कहना है कि .NET में तार एक BStr ("पॉइंटर" से पहले मेमोरी में संग्रहीत लंबाई) और CStr (स्ट्रिंग समाप्त होता है) के संयोजन के रूप में दर्शाए जाते हैं '\ 0')।

स्ट्रिंग "हेलो वहाँ" को इस प्रकार दर्शाया गया है

0B 00 00 00 48 00 65 00 6C 00 6F 00 20 00 74 00 68 00 65 00 72 00 65 00 00 00

(अगर एक करने के लिए आवंटित char*एक में fixed-statement सूचक 0x48 को इंगित होगा।)

यह संरचना एक स्ट्रिंग की लंबाई (कई संदर्भों में उपयोगी) की तेजी से देखने की अनुमति देती है और पॉइंटर को P / Invoke में Win32 (या अन्य) एपीआई में पारित करने की अनुमति देती है जो अशक्त-समाप्त स्ट्रिंग की उम्मीद करते हैं।

जब आप Substring(0, 5)"ओह करते हैं, लेकिन मैंने वादा किया था कि अंतिम चरित्र के बाद एक अशक्त चरित्र होगा" नियम कहता है कि आपको एक प्रतिलिपि बनाने की आवश्यकता है। यहां तक ​​कि अगर आपको अंत में सबस्ट्रिंग मिला है, तो अन्य चर को भ्रष्ट किए बिना लंबाई डालने के लिए कोई जगह नहीं होगी।


कभी-कभी, हालांकि, आप वास्तव में "स्ट्रिंग के मध्य" के बारे में बात करना चाहते हैं, और आप जरूरी नहीं कि पी / इनवॉइस व्यवहार के बारे में परवाह करते हैं। हाल ही में जोड़ी गई ReadOnlySpan<T>संरचना का उपयोग बिना कॉपी के प्रतिस्थापन के लिए किया जा सकता है:

string s = "Hello there";
ReadOnlySpan<char> hello = s.AsSpan(0, 5);
ReadOnlySpan<char> ell = hello.Slice(1, 3);

ReadOnlySpan<char>"सबस्ट्रिंग" भंडार लंबाई स्वतंत्र रूप से, और यह गारंटी मूल्य के अंत के बाद यह है कि वहाँ एक '\ 0' नहीं है। इसका उपयोग कई तरह से किया जा सकता है "एक स्ट्रिंग की तरह", लेकिन यह "स्ट्रिंग" नहीं है क्योंकि इसमें या तो BStr या CStr विशेषताएँ नहीं हैं (दोनों में बहुत कम)। यदि आप कभी भी (सीधे) पी / इनवोक नहीं करते हैं, तो बहुत अंतर नहीं है (जब तक कि जिस एपीआई को आप कॉल करना चाहते हैं उसके पास ReadOnlySpan<char>अधिभार नहीं है )।

ReadOnlySpan<char>संदर्भ प्रकार के क्षेत्र के रूप में उपयोग नहीं किया जा सकता है, इसलिए वहां भी ReadOnlyMemory<char>( s.AsMemory(0, 5)) है, जो कि एक अप्रत्यक्ष तरीका है ReadOnlySpan<char>, इसलिए समान अंतर- stringमौजूद हैं।

पिछले उत्तरों पर दिए गए कुछ उत्तरों / टिप्पणियों के बारे में बात की गई कि कचरा इकट्ठा करने वाले के लिए यह बेकार है कि आपको 5 वर्णों के बारे में बात करना जारी रखने के दौरान एक मिलियन-वर्ण स्ट्रिंग रखना होगा। यह ठीक वही व्यवहार है जो आप ReadOnlySpan<char>दृष्टिकोण के साथ प्राप्त कर सकते हैं । यदि आप केवल छोटी संगणनाएँ कर रहे हैं, तो ReadOnlySpan दृष्टिकोण शायद बेहतर है। यदि आपको इसे थोड़ी देर के लिए जारी रखने की आवश्यकता है और आप मूल स्ट्रिंग का केवल एक छोटा प्रतिशत रखने जा रहे हैं, तो उचित प्रतिस्थापन (अतिरिक्त डेटा को ट्रिम करने के लिए) करना बेहतर है। बीच में कहीं एक संक्रमण बिंदु है, लेकिन यह आपके विशिष्ट उपयोग पर निर्भर करता है।

हमारी साइट का प्रयोग करके, आप स्वीकार करते हैं कि आपने हमारी Cookie Policy और निजता नीति को पढ़ और समझा लिया है।
Licensed under cc by-sa 3.0 with attribution required.