C ++ वैक्टर में amortized निरंतर क्यों push_back है?


23

मैं C ++ सीख रहा हूं और देखा है कि वैक्टर के लिए पुश_बैक फ़ंक्शन के लिए चलने का समय निरंतर है "परिशोधन।" प्रलेखन आगे नोट करता है कि "अगर एक रियलाइजेशन होता है, तो रियलाइजेशन पूरे आकार में रैखिक तक होता है।"

क्या इसका मतलब यह नहीं होना चाहिए कि पुश_बैक फ़ंक्शन , जहां वेक्टर की लंबाई है? आखिरकार, हम सबसे खराब स्थिति विश्लेषण में रुचि रखते हैं, है ना?एनO(n)n

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


रैम मशीन के साथ, मेमोरी के बाइट्स को आवंटित करना एक ऑपरेशन नहीं है - यह बहुत अधिक निरंतर समय माना जाता है। ( एन )nO(n)
usul

जवाबों:


24

यहाँ महत्वपूर्ण शब्द "amortized" है। परिशोधित विश्लेषण एक विश्लेषण तकनीक है जो संचालन के अनुक्रम की जांच करती है । यदि पूरा क्रम समय में चलता है , तो अनुक्रम में प्रत्येक ऑपरेशन में चलता है । यह विचार यह है कि जबकि अनुक्रम में कुछ संचालन महंगा हो सकता है, वे अक्सर कार्यक्रम को कम करने के लिए पर्याप्त नहीं हो सकते हैं। यह ध्यान रखना महत्वपूर्ण है कि यह कुछ इनपुट वितरण या यादृच्छिक विश्लेषण पर औसत केस विश्लेषण से अलग है। अमूर्त विश्लेषण ने इनपुट के बावजूद एक एल्गोरिथ्म के प्रदर्शन के लिए बाध्य सबसे खराब मामला स्थापित किया । यह आमतौर पर डेटा संरचनाओं का विश्लेषण करने के लिए उपयोग किया जाता है, जिनकी पूरे कार्यक्रम में लगातार स्थिति होती है।T ( n ) T ( n ) / nnT(n)T(n)/n

सबसे आम दिया उदाहरणों में से एक एक multipop कार्य है कि पॉप के साथ एक ढेर के विश्लेषण है तत्वों। मल्टीपॉप का एक भोली विश्लेषण यह कहता है कि सबसे खराब स्थिति में मल्टीपॉप को समय लेना चाहिए क्योंकि इससे स्टैक के सभी तत्वों को पॉप करना पड़ सकता है। हालाँकि, यदि आप संचालन के अनुक्रम को देखते हैं, तो आप देखेंगे कि चबूतरे की संख्या धक्का की संख्या से अधिक नहीं हो सकती है। इस प्रकार संचालन के किसी भी क्रम में पॉप की संख्या से अधिक नहीं हो सकती है , और इसलिए समय में मल्टीपॉप चलता है, भले ही कभी-कभी एक कॉल में अधिक समय लग सकता है।O ( n ) n O ( n ) O ( 1 )kO(n)nO(n)O(1)

अब यह C ++ वैक्टर से कैसे संबंधित है? वैक्टर को ऐरे के साथ लागू किया जाता है ताकि एक वेक्टर के आकार को बढ़ाने के लिए आपको मेमोरी को पुनः लोड करना पड़े और पूरे ऐरे को कॉपी करें। जाहिर है हम ऐसा बहुत बार नहीं करना चाहेंगे। इसलिए यदि आप एक पुश_बैक ऑपरेशन करते हैं और वेक्टर को अधिक स्थान आवंटित करने की आवश्यकता होती है, तो यह कारक द्वारा आकार में वृद्धि करेगा । अब यह अधिक मेमोरी लेता है, जिसे आप पूर्ण रूप से उपयोग नहीं कर सकते हैं, लेकिन अगले कुछ पुश_बैक ऑपरेशन सभी निरंतर समय में चलते हैं।m

अब अगर हम पुश_बैक ऑपरेशन (जो मुझे यहां मिला ) का परिशोधित विश्लेषण करते हैं तो हम पाएंगे कि यह निरंतर परिशोधित समय में चलता है। मान लीजिए कि आपके पास आइटम हैं और आपका गुणन कारक । तब रिलोकेशन की संख्या लगभग । वें पुनः आबंटन के लिए आनुपातिक खर्च होंगे , वर्तमान सरणी के आकार के बारे। इस प्रकार पुश बैक के लिए कुल समय , क्योंकि यह एक ज्यामितीय श्रृंखला है। इसे ऑपरेशन से विभाजित करें और हमें पता चलता है कि प्रत्येक ऑपरेशन लेता हैएम लॉग मीटर ( एन ) मैं हूँ मैं n Σ लॉग मीटर ( एन ) मैं = 1 मीटर मैंn मीटरnmlogm(n)imin एनएमi=1logm(n)minmm1n m1m1.5mm1, निरंतर। अंत में आपको अपना फैक्टर चुनने के बारे में सावधान रहना होगा । यदि यह के बहुत करीब है, तो यह निरंतर व्यावहारिक अनुप्रयोगों के लिए बहुत बड़ा हो जाता है, लेकिन यदि बहुत बड़ा है, तो 2 कहें, तो आप बहुत सारी मेमोरी बर्बाद करना शुरू कर देते हैं। आदर्श विकास दर आवेदन द्वारा भिन्न होती है, लेकिन मुझे लगता है कि कुछ कार्यान्वयन उपयोग करते हैं ।m1m1.5


12

हालाँकि @Marc ने (जो मुझे लगता है कि) एक उत्कृष्ट विश्लेषण है, कुछ लोगों को थोड़ा अलग कोण से चीजों पर विचार करना पसंद हो सकता है।

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

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

एक विशिष्ट कार्यान्वयन में अभी भी समान संख्या में प्रतियां हैं - लेकिन एक बार में प्रतियां एक करने के बजाय, यह सभी मौजूदा तत्वों को एक बार में कॉपी करता है। एक तरफ, आप सही हैं: इसका मतलब यह है कि यदि आप push_back के अलग-अलग इनवॉइस को देखते हैं, तो उनमें से कुछ दूसरों की तुलना में काफी धीमी हो जाएंगे। यदि हम एक दीर्घकालिक औसत को देखते हैं, हालांकि, वेक्टर के आकार की परवाह किए बिना, पुश_बैक के प्रति आह्वान की नकल की मात्रा स्थिर रहती है।

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

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

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

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

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


2
+1 यह एक अद्भुत फेनमैन शैली की व्याख्या है।
मोनिका
हमारी साइट का प्रयोग करके, आप स्वीकार करते हैं कि आपने हमारी Cookie Policy और निजता नीति को पढ़ और समझा लिया है।
Licensed under cc by-sa 3.0 with attribution required.