हास्केल और स्कीम एकल-लिंक्ड सूची का उपयोग क्यों करते हैं?


12

एक दोगुनी लिंक की गई सूची में न्यूनतम ओवरहेड (प्रति सेल केवल एक अन्य पॉइंटर) है, और आपको दोनों सिरों को जोड़ने और आगे पीछे जाने की अनुमति मिलती है और आम तौर पर बहुत मज़ा आता है।


सूची निर्माता मूल सूची को संशोधित किए बिना, अकेले जुड़े हुए सूची की शुरुआत में सम्मिलित कर सकता है। यह कार्यात्मक प्रोग्रामिंग के लिए महत्वपूर्ण है। संदेह से जुड़ी सूची में बहुत अधिक संशोधन शामिल हैं, जो बहुत शुद्ध नहीं हैं।
tp1

3
इसके बारे में सोचें, आप एक दोहरी-लिंक की गई अपरिवर्तनीय सूची का भी निर्माण कैसे करेंगे? आपको nextपिछले तत्व बिंदु के prevसूचक को अगले तत्व और अगले तत्व के सूचक को पिछले तत्व को इंगित करने की आवश्यकता है। हालाँकि, उन दो तत्वों में से एक को दूसरे से पहले बनाया गया है, जिसका अर्थ है कि उन तत्वों में से एक को किसी वस्तु की ओर इशारा करने वाले पॉइंटर की आवश्यकता है जो अभी तक मौजूद नहीं है! याद रखें, आप पहले एक तत्व नहीं बना सकते हैं, फिर दूसरा और फिर संकेत सेट करें - वे अपरिवर्तनीय हैं। (नोट: मुझे पता है कि एक तरीका है, आलस्य का शोषण करना, जिसे "टिंट द द नॉट" कहा जाता है।)
जॉर्ग डब्ल्यू मित्तग

1
संदेह-से जुड़ी सूचियाँ आमतौर पर ज्यादातर मामलों में अनावश्यक होती हैं। यदि आपको उन्हें रिवर्स एक्सेस करने की आवश्यकता है, तो स्टैक पर सूची में आइटम पुश करें और उन्हें O (n) रिवर्सल एल्गोरिदम के लिए एक-एक करके पॉप करें।
नील

जवाबों:


23

यदि आप थोड़ा गहराई से देखें, तो वास्तव में आधार भाषा में सरणियाँ शामिल हैं:

  • 5 वीं संशोधित स्कीम रिपोर्ट (R5RS) में वेक्टर प्रकार शामिल है , जो यादृच्छिक आकार के लिए रैखिक समय से बेहतर के साथ निश्चित आकार के पूर्णांक-अनुक्रमित संग्रह हैं।
  • हास्केल 98 रिपोर्ट में एक सरणी प्रकार भी है।

कार्यात्मक प्रोग्रामिंग निर्देश, हालांकि, सरणियों या डबल-लिंक्ड सूचियों पर लंबे समय से एकल-लिंक्ड सूची पर जोर दिया गया है। वास्तव में काफी संभावना है। हालाँकि, इसके कई कारण हैं।

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

data List a           -- A list with element type `a`...
  = Empty             -- is either the empty list...
  | Cell a (List a)   -- or a pair with an `a` and the rest of the list. 

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

map :: (a -> b) -> List a -> List b
map f Empty = Empty
map f (Cell a as) = Cell (f a) (map f as)

filter :: (a -> Bool) -> List a -> List a
filter p Empty = Empty
filter p (Cell a as)
    | p a = Cell a (filter p as)
    | otherwise = filter p as

यह तकनीक इस बात की गारंटी देती है कि आपका कार्य सभी परिमित सूचियों के लिए समाप्त हो जाएगा, और यह एक अच्छी समस्या को सुलझाने की तकनीक भी है - यह स्वाभाविक रूप से समस्याओं को सरल, अधिक टिकाऊ सबपार्ट में विभाजित करता है।

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

दूसरा कारण "एकल-लिंक्ड सूचियों" कारण से कम है, लेकिन "डबल-लिंक्ड सूचियों या सरणियों" से अधिक क्यों नहीं है: कारण यह है कि बाद के डेटा प्रकार अक्सर म्यूटेशन (परिवर्तनीय चर) के लिए कॉल करते हैं , जो कार्यात्मक प्रोग्रामिंग बहुत बार करते हैं से दूर भागता है। जैसा कि होता है:

  • स्कीम जैसी उत्सुक भाषा में आप म्यूटेशन का उपयोग किए बिना एक डबल-लिंक्ड सूची नहीं बना सकते हैं।
  • हास्केल जैसी आलसी भाषा में आप म्यूटेशन का उपयोग किए बिना एक डबल-लिंक्ड सूची बना सकते हैं। लेकिन जब भी आप उस एक के आधार पर एक नई सूची बनाते हैं, तो आपको सबसे अधिक नकल करने के लिए मजबूर किया जाता है यदि मूल की संरचना के सभी नहीं। जबकि एकल-लिंक्ड सूचियों के साथ आप ऐसे कार्य लिख सकते हैं जो "संरचना साझाकरण" का उपयोग करते हैं - कुछ सूचियाँ, जब उपयुक्त हों तो पुरानी सूचियों की कोशिकाओं का पुनः उपयोग कर सकती हैं।
  • परंपरागत रूप से, यदि आप एक अपरिवर्तनीय तरीके से सरणियों का उपयोग करते हैं, तो इसका मतलब है कि हर बार जब आप उस सरणी को संशोधित करना चाहते थे तो आपको पूरी चीज़ की प्रतिलिपि बनानी होगी। (हाल के हास्केल पुस्तकालयों की तरह vector, हालांकि, ऐसी तकनीकें मिली हैं जो इस समस्या पर बहुत सुधार करती हैं)।

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

इसका मतलब है कि पूरी सूची को एक बार में, केवल वर्तमान सेल में मेमोरी में मौजूद होने की आवश्यकता नहीं है। मौजूदा एक से पहले कोशिकाओं को एकत्र किया जा सकता है (जो एक डबल-लिंक्ड सूची के साथ संभव नहीं होगा); वर्तमान में आने वाली कोशिकाओं की गणना तब तक करने की आवश्यकता नहीं है जब तक आप वहां नहीं पहुंच जाते।

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

फ्यूजन भी वह तकनीक है जो उपरोक्त vectorपुस्तकालय अपरिवर्तनीय सरणियों के लिए कुशल कोड उत्पन्न करने के लिए उपयोग करता है। समान बेहद लोकप्रिय bytestring(बाइट सरणियों) और text(यूनिकोड स्ट्रिंग्स) पुस्तकालयों के लिए जाता है, जो हास्केल के नहीं-बहुत-महान देशी Stringप्रकार (जो कि [Char]चरित्र की एकल-लिंक्ड सूची के समान है) के प्रतिस्थापन के रूप में बनाए गए थे । तो आधुनिक हास्केल में एक प्रवृत्ति है जहां संलयन समर्थन के साथ अपरिवर्तनीय सरणी प्रकार बहुत आम हो रहे हैं।

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

बहुत से लोग कार्यात्मक प्रोग्रामिंग वैगन से जल्दी बाहर निकल जाते हैं, इसलिए उन्हें एकल-लिंक की गई सूचियों के लिए एक्सपोज़र मिलता है, लेकिन अधिक उन्नत विचारों के लिए नहीं।


1
क्या शानदार जवाब है!
इलियट गोरोखोवस्की

14

क्योंकि वे अपरिवर्तनीयता के साथ अच्छा काम करते हैं। मान लीजिए कि आपके पास दो अपरिवर्तनीय सूची हैं, [1, 2, 3]और [10, 2, 3]। सूची से जुड़ी प्रत्येक सूची में दर्शाया गया है, जहां सूची में प्रत्येक आइटम एक नोड है जिसमें आइटम और शेष सूची के लिए एक संकेतक है, वे इस तरह दिखेंगे:

node -> node -> node -> empty
 1       2       3

node -> node -> node -> empty
 10       2       3

देखें कि [2, 3]भाग कैसे समान हैं? परिवर्तनशील डेटा संरचनाओं के साथ, वे दो अलग-अलग सूचियां हैं, क्योंकि उनमें से एक के लिए नया डेटा लिखने वाले कोड को दूसरे का उपयोग करके कोड को प्रभावित नहीं करने की आवश्यकता है। हालांकि अपरिवर्तनीय डेटा के साथ , हम जानते हैं कि सूचियों की सामग्री कभी नहीं बदलेगी और कोड नया डेटा नहीं लिख सकता है। इसलिए हम पूंछ का फिर से उपयोग कर सकते हैं और उनकी संरचना के दो सूचियों को साझा कर सकते हैं:

node -> node -> node -> empty
 1      ^ 2       3
        |
node ---+
 10

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

हालांकि, अगर आप कोशिश करते हैं और प्रतिनिधित्व करता है, तो [1, 2, 3]और [10, 2, 3]के रूप में दोगुना से जुड़े हुए सूचियां:

node <-> node <-> node <-> empty
 1       2       3

node <-> node <-> node <-> empty
 10       2       3

अब पूंछ अब समान नहीं हैं। पहले के [2, 3]पास 1सिर पर एक सूचक होता है , लेकिन दूसरे के पास एक सूचक होता है 10। इसके अतिरिक्त, यदि आप सूची के प्रमुख में एक नया आइटम जोड़ना चाहते हैं तो आपको सूची के पिछले प्रमुख को नए सिर पर इंगित करने के लिए बदलना होगा।

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


8
टेल शेयरिंग आपके अनुसार नहीं होता है, हालांकि। आम तौर पर, कोई भी स्मृति में सभी सूचियों से नहीं गुजरता है और आम प्रत्ययों के विलय के अवसरों की तलाश करता है। साझाकरण बस होता है , यह एल्गोरिदम कैसे लिखा जाता है , से बाहर हो जाता है, जैसे कि एक फ़ंक्शन के साथ एक पैरामीटर एक जगह और दूसरे में xsनिर्माण 1:xsकरता है 10:xs

0

@ सैकुंडिम का उत्तर ज्यादातर सही है, लेकिन भाषा डिजाइन और व्यावहारिक आवश्यकताओं के बारे में व्यापार पर कुछ अन्य महत्वपूर्ण अंतर्दृष्टि भी हैं।

वस्तुओं और संदर्भों

ये भाषाएं आमतौर पर अनबाइंड डायनेमिक एक्सटेंड्स (या सी के पार्लेंस, लाइफटाइम , के लिए अनिवार्य हैं) , हालांकि, इन भाषाओं के बीच वस्तुओं के अर्थ के अंतर के कारण ठीक वैसा ही नहीं है , नीचे देखें) डिफ़ॉल्ट रूप से, प्रथम श्रेणी के संदर्भ से परहेज ( जैसे C में ऑब्जेक्ट पॉइंटर्स) और सिमेंटिक रूल्स में अप्रत्याशित व्यवहार (जैसे ISO C का अपरिभाषित व्यवहार से संबंधित व्यवहार)।

इसके अलावा, ऐसी भाषाओं में (प्रथम श्रेणी) वस्तुओं की धारणा रूढ़िवादी रूप से प्रतिबंधात्मक है: कुछ भी नहीं "स्थानिक" गुण डिफ़ॉल्ट रूप से निर्दिष्ट और गारंटीकृत हैं। यह कुछ ALGOL जैसी भाषाओं में पूरी तरह से अलग है, जिनकी वस्तुएं अनबाउंड डायनामिक extents (जैसे C और C ++) में हैं, जहां ऑब्जेक्ट का अर्थ मूल रूप से "टाइप किए गए स्टोरेज" के कुछ प्रकार हैं, जो आमतौर पर मेमोरी स्थानों के साथ युग्मित होते हैं।

वस्तुओं के भीतर भंडारण को एनकोड करने के लिए कुछ अतिरिक्त लाभ हैं जैसे कि अपने पूरे जीवन काल में नियतात्मक कम्प्यूटेशनल प्रभाव संलग्न करने में सक्षम हैं, लेकिन यह एक अन्य विषय है।

डेटा स्ट्रक्चर्स सिमुलेशन की समस्याएं

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

लेकिन वहाँ मौजूद अंतर वेक्टर / सरणी प्रकार बनाम दोहरी-लिंक्ड सूची। एक सरणी को अक्सर O (1) पहुंच समय जटिलता और कम स्थान के उपरि के साथ ग्रहण किया जाता है, जो सूची द्वारा साझा नहीं किए गए उत्कृष्ट गुण हैं। (हालांकि कड़ाई से बोलते हुए, न तो आईएसओ सी द्वारा गारंटी दी जाती है, लेकिन उपयोगकर्ता लगभग हमेशा इसकी उम्मीद करते हैं और कोई व्यावहारिक कार्यान्वयन इन निहितार्थों की गारंटी नहीं देता है, यह स्पष्ट रूप से गारंटी देता है।) ओटीओएच, एक डबल-लिंक्ड सूची अक्सर दोनों गुणों को एक एकल-लिंक्ड सूची से भी बदतर बना देती है। , जबकि पिछड़े / आगे के पुनरावृत्ति भी एक सरणी या एक वेक्टर (पूर्णांक सूचकांकों के साथ) से भी कम ओवरहेड द्वारा समर्थित हैं। इस प्रकार, एक डबल-लिंक्ड सूची सामान्य रूप से बेहतर प्रदर्शन नहीं करती है। और भी बुरा, सूचियों के डायनेमिक मेमोरी आवंटन पर कैश दक्षता और विलंबता के बारे में प्रदर्शन अंतर्निहित कार्यान्वयन वातावरण (उदाहरण libc) द्वारा प्रदान किए गए डिफ़ॉल्ट आवंटनकर्ता का उपयोग करते समय सरणियों / वैक्टरों के प्रदर्शन की तुलना में भयावह रूप से बदतर हैं। तो एक बहुत ही विशिष्ट और "चतुर" रनटाइम के बिना ऐसी वस्तु कृतियों का भारी अनुकूलन, सरणी / वेक्टर प्रकार अक्सर लिंक की गई सूचियों के लिए पसंद किए जाते हैं। (उदाहरण के लिए, आईएसओ सी ++ का उपयोग करते हुए, एक चेतावनी है किstd::vectorstd::listडिफ़ॉल्ट रूप से प्राथमिकता दी जानी चाहिए ।) इस प्रकार, विशेष रूप से समर्थन (दोगुनी) लिंक करने के लिए नई प्राथमिकताओं को पेश करना निश्चित रूप से इतना फायदेमंद नहीं है जितना कि अभ्यास में सरणी / वेक्टर डेटा संरचनाओं का समर्थन करना है।

निष्पक्ष होने के लिए, सूची में अभी भी सरणियों / वैक्टर की तुलना में कुछ विशिष्ट गुण हैं:

  • सूची नोड-आधारित हैं। सूची से तत्वों को हटाने से अन्य नोड्स में अन्य तत्वों के संदर्भ को अमान्य नहीं किया जाता है । (यह कुछ पेड़ या ग्राफ डेटा संरचनाओं के लिए भी सच है।) OTOH, सरणियाँ / वैक्टर अनुगामी स्थिति के संदर्भ को अमान्य बना सकते हैं (कुछ मामलों में बड़े पैमाने पर वसूली के साथ)।
  • सूचियाँ O (1) समय में विभाजित हो सकती हैं । वर्तमान के साथ नए सरणियों / वैक्टरों का पुनर्निर्माण कहीं अधिक महंगा है।

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

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

अपरिवर्तनशीलता और अलियासिंग

हास्केल जैसी शुद्ध भाषा में, वस्तुएँ अपरिवर्तनीय होती हैं। योजना की वस्तु अक्सर म्यूटेशन के बिना उपयोग की जाती है। इस तरह के तथ्य से ऑब्जेक्ट इंटर्निंग के साथ मेमोरी दक्षता को प्रभावी ढंग से सुधारना संभव हो जाता है - मक्खी पर एक ही मूल्य के साथ कई वस्तुओं का अंतर्निहित साझाकरण।

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

सामान्य प्रयोजन के प्रोग्रामिंग के लिए, भाषा डिजाइन की ऐसी पसंद समस्याग्रस्त हो सकती है। लेकिन कुछ सामान्य कार्यात्मक कोडिंग पैटर्न के साथ, भाषाएँ अभी भी अच्छी तरह से काम करती हैं।

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