क्या लिंक्ड नोड संरचना के लिए कोई व्यावहारिक तरीका अपरिवर्तनीय है?


26

मैंने एक एकल-लिंक्ड सूची लिखने का फैसला किया, और आंतरिक लिंक्ड नोड संरचना को अपरिवर्तनीय बनाने के लिए योजना चल रही थी।

मैं हालांकि एक रोड़ा में भाग गया। कहो कि मेरे पास निम्नलिखित लिंक नोड्स हैं (पिछले addऑपरेशन से):

1 -> 2 -> 3 -> 4

और कहते हैं कि मैं एक append करना चाहते हैं 5

ऐसा करने के लिए, चूंकि नोड 4अपरिवर्तनीय है, मुझे इसकी एक नई प्रति बनाने की आवश्यकता है 4, लेकिन इसके nextक्षेत्र को एक नए नोड के साथ बदलें 5। समस्या अब है, 3पुराने को संदर्भित कर रहा है 4; बिना एप्लाइड वाला 5। अब मुझे कॉपी का संदर्भ 3लेने की आवश्यकता है , और कॉपी nextको संदर्भित करने के लिए इसके क्षेत्र को बदलना 4है, लेकिन अब 2पुराने को संदर्भित कर रहा है 3...

या दूसरे शब्दों में, एक परिशिष्ट करने के लिए, पूरी सूची की प्रतिलिपि बनाने की आवश्यकता लगती है।

मेरे सवाल:

  • क्या मेरी सोच सही है? क्या संपूर्ण संरचना की नकल के बिना एक परिशिष्ट करने का कोई तरीका है?

  • जाहिरा तौर पर "प्रभावी जावा" में शामिल हैं:

    जब तक वहाँ एक बहुत अच्छा कारण नहीं है कि उन्हें उत्परिवर्तित करने के लिए कक्षाएं अपरिवर्तनीय होनी चाहिए ...

    क्या यह पारस्परिकता के लिए एक अच्छा मामला है?

मुझे नहीं लगता कि यह सुझाए गए उत्तर की नकल है क्योंकि मैं सूची के बारे में बात नहीं कर रहा हूं; यह स्पष्ट रूप से इंटरफेस के अनुरूप है (नई सूची को आंतरिक रूप से रखने और इसे पाने वाले की तरह पुनः प्राप्त करने के बिना कुछ करने के बिना, दूसरे विचार पर, हालांकि, यहां तक ​​कि कुछ उत्परिवर्तन की आवश्यकता होगी; यह सिर्फ एक न्यूनतम तक रखा जाएगा)। मैं इस बारे में बात कर रहा हूं कि सूची के आंतरिक लोगों को अपरिवर्तनीय होना चाहिए या नहीं।


मैं कहूँगा हाँ - जावा में दुर्लभ अपरिवर्तनीय संग्रह या तो फेंकने के लिए उत्परिवर्ती तरीकों के साथ वाले हैं, या CopyOnWritexxxबहु-थ्रेडिंग के लिए उपयोग किए जाने वाले अविश्वसनीय रूप से महंगे वर्ग हैं। किसी को भी उम्मीद नहीं है कि संग्रह वास्तव में अपरिवर्तनीय होगा (हालाँकि यह कुछ विचित्रता पैदा करता है)
क्रमांक

9
(उद्धरण पर टिप्पणी करें।) यह, एक महान उद्धरण, अक्सर बहुत गलत समझा जाता है। इसके लाभ के कारण, वैल्यूऑबजेक्ट में अपरिवर्तनीयता लागू की जानी चाहिए , लेकिन यदि आप जावा में विकसित कर रहे हैं, तो उन जगहों पर अपरिवर्तनीयता का उपयोग करना अव्यावहारिक है जहां परिवर्तनशीलता का अनुमान है। अधिकांश समय, एक वर्ग के कुछ पहलू होते हैं जिन्हें अपरिवर्तनीय होना चाहिए, और कुछ ऐसे पहलू जो आवश्यकताओं के आधार पर परस्पर होने चाहिए।
rwong

2
संबंधित (संभवतः एक डुप्लिकेट): क्या संग्रह वस्तु बेहतर अपरिवर्तनीय है? "प्रदर्शन ओवरहेड्स के बिना, इस संग्रह (वर्ग लिंक्डलिस्ट) को अपरिवर्तनीय संग्रह के रूप में पेश किया जा सकता है?"
gnat

आप एक अपरिवर्तनीय लिंक्ड सूची क्यों बनाएंगे? लिंक की गई सूची का सबसे बड़ा लाभ उत्परिवर्तन है, क्या आप एक सरणी के साथ बेहतर नहीं होंगे?
पीटर बी

3
क्या आप कृपया मुझे अपनी आवश्यकताओं को समझने में मदद कर सकते हैं? आप एक अपरिवर्तनीय सूची रखना चाहते हैं, जिसे आप बदलना चाहते हैं? क्या यह विरोधाभास नहीं है? सूची के "आंतरिक" के साथ आपका क्या मतलब है? क्या आपका मतलब है कि पहले जोड़े गए नोड्स को बाद में संशोधित नहीं किया जाना चाहिए, लेकिन केवल सूची में अंतिम नोड को जोड़ने की अनुमति है?
null

जवाबों:


46

कार्यात्मक भाषाओं में सूचियों के साथ, आप लगभग हमेशा एक सिर और पूंछ के साथ काम करते हैं, पहला तत्व और शेष सूची। Prepending बहुत अधिक सामान्य है क्योंकि, जैसा कि आपने surmised किया है, appending के लिए पूरी सूची (या अन्य आलसी डेटा संरचनाओं की प्रतिलिपि बनाने की आवश्यकता होती है जो लिंक की गई सूची से ठीक से मेल नहीं खाती हैं)।

अनिवार्य भाषाओं में, जोड़ना अधिक आम है, क्योंकि यह शब्दार्थ को और अधिक स्वाभाविक महसूस करता है, और आप सूची के पिछले संस्करणों के संदर्भों को अमान्य करने के बारे में परवाह नहीं करते हैं।

इस बात के उदाहरण के लिए कि पूर्व-सूची में पूरी सूची की प्रतिलिपि बनाने की आवश्यकता क्यों नहीं है, पर विचार करें:

2 -> 3 -> 4

आपको एक प्रस्ताव देना 1:

1 -> 2 -> 3 -> 4

लेकिन ध्यान दें कि यह कोई फर्क नहीं पड़ता कि कोई और अभी भी 2अपनी सूची के प्रमुख के रूप में एक संदर्भ धारण कर रहा है , क्योंकि सूची अपरिवर्तनीय है और लिंक केवल एक ही रास्ते पर चलते हैं। 1अगर आपके पास केवल एक संदर्भ है तो भी बताने का कोई तरीका नहीं है 2। अब, यदि आपने 5किसी सूची में शामिल किया है, तो आपको पूरी सूची की एक प्रति बनानी होगी, क्योंकि अन्यथा यह दूसरी सूची में भी दिखाई देगी।


आह, क्यों prepending एक प्रतिलिपि की आवश्यकता नहीं है पर अच्छा बिंदु; मैंने ऐसा नहीं सोचा था।
कारजीनिकेट

17

यदि आप किसी भी नोड को इन-प्लेस करने के लिए तैयार नहीं हैं, तो आप सही हैं, पूरी सूची को कॉपी करने की आवश्यकता है। इसके लिए हमें next(अब) दूसरे-से-अंतिम नोड के पॉइंटर को सेट करना होगा , जो एक अपरिवर्तनीय सेटिंग में एक नया नोड बनाता है, और फिर हमें nextतीसरे-से-पिछले नोड के पॉइंटर को सेट करने की आवश्यकता होती है , और इसी तरह।

मुझे लगता है कि यहां मुख्य समस्या अपरिवर्तनीयता नहीं है, न ही appendऑपरेशन की सलाह है। दोनों अपने-अपने डोमेन में बिल्कुल ठीक हैं। उनका मिश्रण करना बुरा है: एक अपरिवर्तनीय सूची के लिए प्राकृतिक (कुशल) इंटरफ़ेस ने सूची के सामने हेरफेर पर जोर दिया, लेकिन उत्परिवर्तित सूचियों के लिए पहले से आखिरी तक वस्तुओं को क्रमिक रूप से जोड़कर सूची का निर्माण करना अधिक स्वाभाविक है।

इसलिए मैं सुझाव देता हूं कि आप अपना मन बना लें: क्या आप एक अल्पकालिक इंटरफ़ेस या एक निरंतर चाहते हैं? क्या अधिकांश ऑपरेशन एक नई सूची का निर्माण करते हैं और एक अनमॉडिफाइड संस्करण को सुलभ (लगातार) छोड़ देते हैं, या क्या आप इस तरह का कोड लिखते हैं (पंचांग):

list.append(x);
list.prepend(y);

दोनों विकल्प ठीक हैं, लेकिन कार्यान्वयन को इंटरफ़ेस को प्रतिबिंबित करना चाहिए: एक स्थिर डेटा संरचना को अपरिवर्तनीय नोड्स से लाभ होता है, जबकि एक अल्पकालिक को वास्तव में प्रदर्शन के वादे को पूरा करने के लिए आंतरिक पारस्परिकता की आवश्यकता होती है। java.util.Listऔर अन्य इंटरफेस अल्पकालिक हैं: एक अपरिवर्तनीय सूची पर उन्हें लागू करना अनुचित है और वास्तव में एक प्रदर्शन खतरा है। परिवर्तनशील डेटा संरचनाओं पर अच्छे एल्गोरिदम अक्सर अपरिवर्तनीय डेटा संरचनाओं पर अच्छे एल्गोरिदम से काफी भिन्न होते हैं, इसलिए एक अपरिवर्तनीय डेटा संरचना को एक उत्परिवर्ती (या इसके विपरीत) के रूप में ड्रेसिंग करना खराब एल्गोरिदम को आमंत्रित करता है।

जबकि एक सतत सूची में कुछ नुकसान (कोई कुशल एपेंडिंग) नहीं है, कार्यात्मक रूप से प्रोग्रामिंग करते समय यह एक गंभीर समस्या नहीं है: कई एल्गोरिदम को कुशलतापूर्वक मानसिकता को स्थानांतरित करने और उच्च-क्रम के कार्यों का उपयोग करके mapया fold( जैसे दो अपेक्षाकृत सकारात्मक नाम रखने के लिए) तैयार किया जा सकता है। ), या बार-बार बहाने से । इसके अलावा, कोई भी आपको केवल इस डेटा संरचना का उपयोग करने के लिए मजबूर नहीं कर रहा है: जब अन्य (पंचांग, ​​या लगातार लेकिन अधिक परिष्कृत) अधिक उपयुक्त होते हैं, तो उनका उपयोग करें। मुझे यह भी ध्यान देना चाहिए कि लगातार सूचियों में अन्य वर्कलोड के लिए कुछ फायदे हैं: वे अपनी पूंछ साझा करते हैं, जो स्मृति को संरक्षित कर सकते हैं।


4

यदि आपके पास एक एकल लिंक की गई सूची है, तो आप सामने वाले के साथ काम कर रहे होंगे यदि यह आपकी पीठ से अधिक होगा।

प्रोलॉग और हैकेल जैसी कार्यात्मक भाषाएं सामने वाले तत्व और बाकी सरणी को प्राप्त करने के लिए आसान तरीके प्रदान करती हैं। प्रत्येक नोड की प्रतियों के साथ O (n) ऑपरेशन में पीछे की ओर लगाना है।


1
मैंने हास्केल का उपयोग किया है। अफीक, यह आलस का उपयोग करके समस्या का हिस्सा भी बनता है। मैं एपेंड कर रहा हूं क्योंकि मुझे लगा कि Listइंटरफेस से यही उम्मीद है (हालांकि मैं वहां गलत हो सकता हूं)। मुझे नहीं लगता कि बिट वास्तव में इस सवाल का जवाब देता है। पूरी सूची को अभी भी कॉपी करने की आवश्यकता होगी; यह केवल अंतिम तत्व तक तेजी से पहुंच बना देगा क्योंकि पूर्ण ट्रैवर्सल की आवश्यकता नहीं होगी।
कारजिनेट

एक इंटरफ़ेस के अनुसार एक क्लास बनाना जो अंतर्निहित डेटा संरचना / एल्गोरिदम से मेल नहीं खाता है, दर्द और अक्षमताओं के लिए एक निमंत्रण है।
शाफ़्ट फ्रीक

JavaDocs स्पष्ट रूप से "परिशिष्ट" शब्द का उपयोग करता है। आप कह रहे हैं कि कार्यान्वयन के लिए इसे अनदेखा करना बेहतर है?
Cargigenicate

2
@Carcigenicate नहीं मैं कह रहा हूँ कि यह कोशिश करने के लिए एक गलती है और अपरिवर्तनीय नोड्स के सांचे के साथ एक एकल लिंक वाली सूची को फिट करने के लिएjava.util.List
शाफ़्ट सनकी

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

4

जैसा कि दूसरों ने बताया है, आप सही हैं कि एक अपरिवर्तनीय एकल-लिंक्ड सूची में परिशिष्ट संचालन करते समय पूरी सूची की प्रतिलिपि बनाने की आवश्यकता होती है।

अक्सर आप cons(प्रीपेंड) संचालन के संदर्भ में अपने एल्गोरिथ्म को लागू करने के समाधान का उपयोग कर सकते हैं , और फिर एक बार अंतिम सूची को उल्टा कर सकते हैं। इसे अभी भी एक बार सूची को कॉपी करने की आवश्यकता है, लेकिन सूची की लंबाई में जटिलता ओवरहेड रैखिक है, जबकि बार-बार परिशिष्ट का उपयोग करके आप आसानी से एक द्विघात जटिलता प्राप्त कर सकते हैं।

अंतर सूचियां (उदाहरण के लिए यहां देखें ) एक दिलचस्प विकल्प हैं। एक अंतर सूची एक सूची लपेटती है और निरंतर समय में एक परिशिष्ट ऑपरेशन प्रदान करती है। आप मूल रूप से रैपर के साथ तब तक काम करते हैं जब तक आपको एपेंड करने की आवश्यकता होती है, और फिर समाप्त होने पर एक सूची में वापस कन्वर्ट करें। जब आप StringBuilderस्ट्रिंग का निर्माण करने के लिए उपयोग करते हैं तो यह किसी तरह से होता है , और अंत Stringमें कॉल करके परिणाम (अपरिवर्तनीय!) के रूप में मिलता है toString। एक अंतर यह है कि एक StringBuilderउत्परिवर्तनीय है, लेकिन एक अंतर सूची अपरिवर्तनीय है। इसके अलावा, जब आप एक अंतर सूची को वापस एक सूची में परिवर्तित करते हैं, तो आपको अभी भी पूरी नई सूची का निर्माण करना होगा लेकिन, फिर से, आपको केवल एक बार करने की आवश्यकता है।

एक अपरिवर्तनीय DListवर्ग को लागू करना काफी आसान होना चाहिए जो हास्केल के समान इंटरफ़ेस प्रदान करता है Data.DList


4

आप 2015 के इस बेहतरीन वीडियो को Immutable.js के निर्माता ली बायरन द्वारा रियेक्ट कॉन्फिडेंस से देखें । यह आपको एक कुशल अपरिवर्तनीय सूची को लागू करने के तरीके को समझने के लिए संकेत और संरचना देगा, जो सामग्री की नकल नहीं करता है। मूल विचार हैं: - जब तक दो सूचियाँ समान नोड्स (समान मान, समान अगला नोड) का उपयोग करती हैं, तब तक समान नोड का उपयोग किया जाता है - जब सूचियाँ अलग-अलग होने लगती हैं, तो एक संरचना विचलन के नोड पर बनाई जाती है, जो बिंदुओं को रखती है प्रत्येक सूची के अगले विशेष नोड

एक प्रतिक्रिया ट्यूटोरियल से यह छवि मेरी टूटी हुई अंग्रेजी की तुलना में स्पष्ट हो सकती है:यहाँ छवि विवरण दर्ज करें


2

यह कड़ाई से जावा नहीं है, लेकिन मेरा सुझाव है कि आप इस लेख को एक अपरिवर्तनीय पर पढ़ें, लेकिन स्कैन्ड में लिखे गए निरंतर अनुक्रमित डेटा संरचना को निष्पादित करते हैं:

http://www.codecommit.com/blog/scala/implementing-persistent-vectors-in-scala

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

इसके अलावा, अपरिवर्तनीय डेटा संरचनाओं के निर्माण पर एक नोट : आप जो चाहते हैं, वह आमतौर पर "बिल्डर" का कुछ प्रकार होता है, जो आपको तत्वों को जोड़कर (एक ही थ्रेड के भीतर) "आपके" निर्माणाधीन "डेटा संरचना" को म्यूट करने की अनुमति देता है; आपके द्वारा अपील किए जाने के बाद, आप "अंडर-कंस्ट्रक्शन" ऑब्जेक्ट पर एक विधि कहते हैं जैसे कि , .build()या .result()जो "ऑब्जेक्ट बनाता है", आपको एक अपरिवर्तनीय डेटा संरचना देता है जिसे आप तब सुरक्षित रूप से साझा कर सकते हैं।


1
वहाँ कम से PersistentVector के जावा बंदरगाहों के बारे में सवाल है stackoverflow.com/questions/15997996/...
James_pic

1

एक दृष्टिकोण जो कभी-कभी मददगार हो सकता है, वह यह होगा कि सूची-धारण करने वाली वस्तुओं के दो वर्ग हो सकते हैं - finalआगे-जुड़ी सूची में पहले नोड के संदर्भ में एक अग्र-सूची ऑब्जेक्ट और एक प्रारंभिक-अशक्त गैर- finalसंदर्भ जो (जब गैर -null) एक रिवर्स-लिस्ट ऑब्जेक्ट को विपरीत क्रम में समान आइटम पकड़े हुए, और रिवर्स-लिस्ट ऑब्जेक्ट finalके अंतिम आइटम के संदर्भ में एक रिवर्स-लिस्ट ऑब्जेक्ट की पहचान करता है और एक प्रारंभिक-अशक्त गैर-अंतिम संदर्भ जो (जब गैर -नुल) समान वस्तुओं को विपरीत क्रम में रखने वाली एक अग्र-सूची वस्तु की पहचान करता है।

किसी आइटम को अग्रेषित-सूची में जोड़ना या किसी आइटम को रिवर्स-लिस्ट में जोड़ना केवल एक नया नोड बनाने की आवश्यकता होगी जो finalसंदर्भ द्वारा पहचाने गए नोड से लिंक करता है और मूल के रूप में एक ही प्रकार की नई सूची ऑब्जेक्ट finalबनाता है। उस नए नोड के लिए।

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

ध्यान दें कि किसी सूची ऑब्जेक्ट की बाहरी स्थिति को एक ही माना जाएगा कि क्या इसके विपरीत-प्रकार की सूची का संदर्भ शून्य है या एक विपरीत-क्रम सूची की पहचान करता है। finalमल्टी-थ्रेडेड कोड का उपयोग करते समय भी कोई चर बनाने की आवश्यकता नहीं होगी , क्योंकि प्रत्येक सूची ऑब्जेक्ट finalमें इसकी सामग्री की पूरी प्रतिलिपि का संदर्भ होगा ।

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

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