पूंछ पुनरावृत्ति क्या है?


1692

जबकि लिस्प सीखना शुरू कर दिया है, मैं पूंछ-पुनरावर्ती शब्द पर आया हूं । इसका सही मतलब क्या है?


153
जिज्ञासु के लिए: दोनों जबकि और समय के लिए भाषा में बहुत लंबे समय से हैं। जबकि पुरानी अंग्रेजी में उपयोग में था; whilst का मध्य अंग्रेजी विकास है। अर्थ के रूप में वे विनिमेय हैं, लेकिन मानक अमेरिकी अंग्रेजी में whilst जीवित नहीं रहा है।
फिलीप बार्टूज़ी

14
शायद यह देर हो चुकी है, लेकिन पूंछ पुनरावृत्ति के बारे में यह एक बहुत अच्छा लेख है: programmerinterview.com/index.php/recursion/tail-recursion
Sam003

5
पूंछ-पुनरावर्ती कार्य की पहचान करने के महान लाभों में से एक यह है कि इसे पुनरावृत्त रूप में परिवर्तित किया जा सकता है और इस प्रकार विधि-स्टैक-ओवरहेड से एल्गोरिथ्म को राहत दी जा सकती है। @ केइल क्रोनिन और नीचे कुछ अन्य से प्रतिक्रिया की यात्रा करना पसंद कर सकते हैं
केजहतक

@Yesudeep का यह लिंक सबसे अच्छा, सबसे विस्तृत विवरण मैंने पाया है - lua.org/pil/6.3.html
जेफ फिशर

1
क्या कोई मुझे बता सकता है, क्या मर्ज सॉर्ट और क्विक सॉर्ट यूज़ टेल रिकर्सियन (TRO) का उपयोग करता है?
मजूरगेटर्टन

जवाबों:


1717

पहले N प्राकृतिक संख्याओं को जोड़ने वाले एक साधारण कार्य पर विचार करें। (जैसे sum(5) = 1 + 2 + 3 + 4 + 5 = 15)।

यहाँ एक सरल जावास्क्रिप्ट कार्यान्वयन है जो पुनरावर्तन का उपयोग करता है:

function recsum(x) {
    if (x === 1) {
        return x;
    } else {
        return x + recsum(x - 1);
    }
}

यदि आप कहते हैं recsum(5), तो यह है कि जावास्क्रिप्ट दुभाषिया मूल्यांकन करेगा:

recsum(5)
5 + recsum(4)
5 + (4 + recsum(3))
5 + (4 + (3 + recsum(2)))
5 + (4 + (3 + (2 + recsum(1))))
5 + (4 + (3 + (2 + 1)))
15

ध्यान दें कि जावास्क्रिप्ट दुभाषिया शुरू होने से पहले प्रत्येक पुनरावर्ती कॉल को कैसे पूरा करना है, वास्तव में राशि की गणना का काम करते हैं।

यहाँ एक ही फ़ंक्शन का टेल-पुनरावर्ती संस्करण है:

function tailrecsum(x, running_total = 0) {
    if (x === 0) {
        return running_total;
    } else {
        return tailrecsum(x - 1, running_total + x);
    }
}

यहां उन घटनाओं का क्रम है जो आपको बुलाया जाता है tailrecsum(5), (जो tailrecsum(5, 0)कि डिफ़ॉल्ट रूप से दूसरे तर्क के कारण प्रभावी रूप से होगा )।

tailrecsum(5, 0)
tailrecsum(4, 5)
tailrecsum(3, 9)
tailrecsum(2, 12)
tailrecsum(1, 14)
tailrecsum(0, 15)
15

पुनरावर्ती कॉल के प्रत्येक मूल्यांकन के साथ पूंछ-पुनरावर्ती मामले में, running_totalअद्यतन किया जाता है।

नोट: मूल उत्तर ने पायथन से उदाहरणों का उपयोग किया। इन्हें जावास्क्रिप्ट में बदल दिया गया है, क्योंकि पायथन दुभाषिए पूंछ कॉल अनुकूलन का समर्थन नहीं करते हैं । हालाँकि, जबकि टेल कॉल ऑप्टिमाइज़ेशन ECMAScript 2015 युक्ति का हिस्सा है , अधिकांश जावास्क्रिप्ट दुभाषिए इसका समर्थन नहीं करते हैं


32
क्या मैं कह सकता हूं कि पूंछ पुनरावृत्ति के साथ अंतिम उत्तर की गणना अकेले विधि के पिछले आह्वान द्वारा की जाती है? यदि यह पूंछ पुनरावृत्ति नहीं है, तो आपको उत्तर की गणना करने के लिए सभी विधि के लिए सभी परिणामों की आवश्यकता होगी।
च्रिसपोटेक

2
यहाँ एक परिशिष्ट है जो Lua में कुछ उदाहरण प्रस्तुत करता है: lua.org/pil/6.3.html इस माध्यम से जाने के लिए उपयोगी हो सकता है! :)
यसदीप

2
क्या कोई कृपया क्रिसपोटेक के प्रश्न को संबोधित कर सकता है? मैं असमंजस में हूँ tail recursionकि ऐसी भाषा में कैसे प्राप्त किया जा सकता है जो टेल कॉल का अनुकूलन नहीं करती है।
केविन मेरेडिथ

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

2
तो अजगर में कोई फायदा नहीं है क्योंकि टेलरेस्कुम फ़ंक्शन के लिए हर कॉल के साथ, एक नया स्टैक फ्रेम बनाया जाता है - सही है?
क़ाज़ी इरफ़ान

707

में पारंपरिक प्रत्यावर्तन , विशिष्ट मॉडल है कि आप अपने पुनरावर्ती कॉल पहला प्रदर्शन, और फिर आप पुनरावर्ती कॉल के रिटर्न मान लेते हैं और परिणाम की गणना है। इस तरीके से, आपको अपनी गणना का परिणाम नहीं मिलता है जब तक कि आप हर पुनरावर्ती कॉल से वापस नहीं आए।

में पूंछ प्रत्यावर्तन , तो आप अपने गणना पहले करते हैं, और फिर आप पुनरावर्ती कॉल पर अमल, अगले पुनरावर्ती कदम के लिए अपने वर्तमान कदम के परिणाम गुजर। यह अंतिम विवरण के रूप में होता है (return (recursive-function params))मूल रूप से, किसी भी दिए गए पुनरावर्ती चरण का वापसी मूल्य अगले पुनरावर्ती कॉल के वापसी मूल्य के समान है

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


17
"मुझे पूरा यकीन है कि लिस्प ऐसा करता है" - स्कीम करती है, लेकिन कॉमन लिस्प हमेशा नहीं करता है।
हारून

2
@ डैनियल "मूल रूप से, किसी भी दिए गए पुनरावर्ती कदम का वापसी मूल्य अगले नर्स की वापसी मूल्य के समान है।" - मैं लोरिन होचस्टीन द्वारा पोस्ट किए गए कोड स्निपेट के लिए इस तर्क को सच होते हुए देखने में विफल हूं। क्या आप कृपया विस्तार से बता सकते हैं?
गीक

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

3
स्काला करता है, लेकिन आपको इसे लागू करने के लिए निर्दिष्ट @tailrec की आवश्यकता है।
साइलेंटडिज

2
"इस तरीके से, आपको अपनी गणना का परिणाम तब तक नहीं मिलता है जब तक कि आप हर पुनरावर्ती कॉल से वापस नहीं आते हैं।" - शायद मैं इसे गलत समझ रहा हूं, लेकिन यह आलसी भाषाओं के लिए विशेष रूप से सच नहीं है, जहां पारंपरिक पुनरावर्तन वास्तव में सभी पुनरावर्ती (जैसे && के साथ बूल की एक अनंत सूची पर तह) के बिना एक परिणाम प्राप्त करने का एकमात्र तरीका है।
हैफेल

205

एक महत्वपूर्ण बिंदु यह है कि पूंछ की पुनरावृत्ति अनिवार्य रूप से लूपिंग के बराबर है। यह केवल कंपाइलर ऑप्टिमाइज़ेशन की बात नहीं है, बल्कि अभिव्यक्ति के बारे में एक बुनियादी तथ्य है। यह दोनों तरीके से जाता है: आप फॉर्म का कोई भी लूप ले सकते हैं

while(E) { S }; return Q

जहां Eऔर Qअभिव्यक्ति हैं और Sबयानों का एक क्रम है, और इसे एक पूंछ पुनरावर्ती फ़ंक्शन में बदल दें

f() = if E then { S; return f() } else { return Q }

बेशक, E, S, और Qकुछ चर पर कुछ दिलचस्प मूल्य की गणना करने के लिए परिभाषित किया जाना है। उदाहरण के लिए, लूपिंग फ़ंक्शन

sum(n) {
  int i = 1, k = 0;
  while( i <= n ) {
    k += i;
    ++i;
  }
  return k;
}

टेल-पुनरावर्ती फ़ंक्शन (एस) के बराबर है

sum_aux(n,i,k) {
  if( i <= n ) {
    return sum_aux(n,i+1,k+i);
  } else {
    return k;
  }
}

sum(n) {
  return sum_aux(n,1,0);
}

(कम मापदंडों के साथ एक फ़ंक्शन के साथ पूंछ-पुनरावर्ती फ़ंक्शन का यह "रैपिंग" एक सामान्य कार्यात्मक मुहावरा है)


@LorinHochstein के उत्तर में मैंने समझा, उनके स्पष्टीकरण के आधार पर, यह पुनरावृत्ति तब होती है जब पुनरावर्ती भाग "रिटर्न" का अनुसरण करता है, हालाँकि आप में, पूंछ पुनरावर्ती नहीं है। क्या आप सुनिश्चित हैं कि आपके उदाहरण को पूंछ पुनरावृत्ति माना जाता है?
कोड़ीबगस्टीन

1
@ इमर्रे टेल-पुनरावर्ती भाग sum_aux के अंदर "वापसी sum_aux" कथन है।
क्रिस कॉनवे

1
@ लम्रे: क्रिस का कोड अनिवार्य रूप से समकक्ष है। सीमित परीक्षण के अगर / फिर और शैली के क्रम ... अगर x == 0 बनाम यदि (i <= n) ... पर लटका पाने के लिए कुछ नहीं है। मुद्दा यह है कि प्रत्येक पुनरावृत्ति इसके परिणाम को अगले तक पहुंचाती है।
टेलर

else { return k; }बदला जा सकता हैreturn k;
c0der

144

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

एक पूंछ कॉल [पूंछ पुनरावृत्ति] कॉल के रूप में तैयार की गई गोटो का एक प्रकार है। एक टेल कॉल तब होता है जब एक फ़ंक्शन दूसरे को अपनी अंतिम क्रिया के रूप में बुलाता है, इसलिए इसके पास और कुछ नहीं है। उदाहरण के लिए, निम्नलिखित कोड में, कॉल gटेल कॉल है:

function f (x)
  return g(x)
end

fकॉल करने के बाद g, इसके पास और कुछ नहीं है। ऐसी स्थितियों में, प्रोग्राम को कॉलिंग फ़ंक्शन पर वापस जाने की आवश्यकता नहीं होती है जब तथाकथित फ़ंक्शन समाप्त होता है। इसलिए, पूंछ कॉल के बाद, प्रोग्राम को स्टैक में कॉलिंग फ़ंक्शन के बारे में कोई जानकारी रखने की आवश्यकता नहीं है। ...

क्योंकि एक उचित टेल कॉल कोई स्टैक स्पेस का उपयोग नहीं करता है, "नेस्टेड" टेल कॉल की संख्या पर कोई सीमा नहीं है जो एक प्रोग्राम बना सकता है। उदाहरण के लिए, हम किसी भी संख्या के साथ निम्न फ़ंक्शन को तर्क कह सकते हैं; यह स्टैक को कभी भी ओवरफ्लो नहीं करेगा:

function foo (n)
  if n > 0 then return foo(n - 1) end
end

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

यह खेल एक विशिष्ट राज्य मशीन है, जहां वर्तमान कमरा राज्य है। हम प्रत्येक कमरे के लिए एक फ़ंक्शन के साथ इस तरह के भूलभुलैया को लागू कर सकते हैं। हम एक कमरे से दूसरे कमरे में जाने के लिए टेल कॉल का उपयोग करते हैं। चार कमरों वाला एक छोटा भूलभुलैया इस तरह दिख सकता है:

function room1 ()
  local move = io.read()
  if move == "south" then return room3()
  elseif move == "east" then return room2()
  else print("invalid move")
       return room1()   -- stay in the same room
  end
end

function room2 ()
  local move = io.read()
  if move == "south" then return room4()
  elseif move == "west" then return room1()
  else print("invalid move")
       return room2()
  end
end

function room3 ()
  local move = io.read()
  if move == "north" then return room1()
  elseif move == "east" then return room4()
  else print("invalid move")
       return room3()
  end
end

function room4 ()
  print("congratulations!")
end

तो आप देखते हैं, जब आप एक पुनरावर्ती कॉल करते हैं जैसे:

function x(n)
  if n==0 then return 0
  n= n-2
  return x(n) + 1
end

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


9
यह एक महान जवाब है क्योंकि यह स्टैक आकार पर पूंछ कॉल के निहितार्थ की व्याख्या करता है।
एंड्रयू स्वान

@AndrewSwan वास्तव में, हालांकि मेरा मानना ​​है कि मूल प्रश्नकर्ता और सामयिक पाठक जो इस प्रश्न पर ठोकर खा सकते हैं, उन्हें स्वीकार किए गए उत्तर के साथ बेहतर सेवा दी जा सकती है (क्योंकि वह नहीं जान सकते कि वास्तव में स्टैक क्या है।) वैसे मैं जीरा का उपयोग करता हूं, बड़ा। पंखा।
हॉफमैन

1
मेरे पसंदीदा उत्तर के साथ-साथ स्टैक आकार के निहितार्थ को शामिल करने के कारण।
njk2015

80

नियमित पुनरावर्तन का उपयोग करते हुए, प्रत्येक पुनरावर्ती कॉल कॉल स्टैक पर एक और प्रविष्टि को धक्का देता है। जब पुनरावृत्ति पूरी हो जाती है, तो ऐप को प्रत्येक प्रविष्टि को वापस नीचे सभी तरह से पॉप करना होगा।

पूंछ पुनरावृत्ति के साथ, भाषा के आधार पर कंपाइलर एक प्रविष्टि के नीचे स्टैक को गिराने में सक्षम हो सकता है, जिससे आप स्टैक स्पेस को बचाते हैं ... एक बड़ी पुनरावर्ती क्वेरी वास्तव में स्टैक ओवरफ़्लो का कारण बन सकती है।

मूल रूप से टेल रिक्रिएशन को पुनरावृति में अनुकूलित किया जा सकता है।


1
"एक बड़ी पुनरावर्ती क्वेरी वास्तव में स्टैक ओवरफ्लो का कारण बन सकती है।" 1 पैरा में होना चाहिए, दूसरे में नहीं (पूंछ पुनरावृत्ति) एक? पूंछ पुनरावृत्ति का बड़ा लाभ यह है कि यह (पूर्व: योजना) इस तरह से अनुकूलित किया जा सकता है जैसे कि स्टैक में कॉल "संचित" नहीं करना है, इसलिए ज्यादातर स्टैक ओवरफ्लो से बचेंगे!
ओलिवियर दुलैक

69

शब्दजाल फ़ाइल में पूंछ पुनरावृत्ति की परिभाषा के बारे में कहा गया है:

पूंछ पुनरावर्तन / एन /

यदि आप पहले से ही इसके बारे में बीमार नहीं हैं, तो पूंछ पुनरावृत्ति देखें।


68

इसे शब्दों से समझाने के बजाय, यहां एक उदाहरण दिया गया है। यह फैक्टरियल फ़ंक्शन का एक योजना संस्करण है:

(define (factorial x)
  (if (= x 0) 1
      (* x (factorial (- x 1)))))

यहाँ गुटबाजी का एक संस्करण है जो पूंछ-पुनरावर्ती है:

(define factorial
  (letrec ((fact (lambda (x accum)
                   (if (= x 0) accum
                       (fact (- x 1) (* accum x))))))
    (lambda (x)
      (fact x 1))))

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


4
+1 पूंछ-पुनरावृत्ति के सबसे महत्वपूर्ण पहलू का उल्लेख करने के लिए कि उन्हें एक पुनरावृत्त रूप में परिवर्तित किया जा सकता है और इस तरह इसे ओ (1) स्मृति जटिलता रूप में बदल दिया जा सकता है।
केजातक

1
@ किटकैट बिल्कुल नहीं; जवाब सही ढंग से "स्थिर स्टैक स्पेस" के बारे में बोलता है, सामान्य रूप से मेमोरी नहीं। नाइटपैकिंग नहीं होना चाहिए, बस यह सुनिश्चित करने के लिए कि कोई गलतफहमी नहीं है। उदाहरण के लिए, टेल-पुनरावर्ती सूची-टेल-म्यूटिंग list-reverseप्रक्रिया निरंतर स्टैक स्पेस में चलेगी लेकिन ढेर पर डेटा संरचना बनाएगी और बढ़ेगी। एक ट्री ट्रैवरल एक अतिरिक्त तर्क में एक नकली स्टैक का उपयोग कर सकता है। आदि
विल नेस

45

पुनरावर्ती एल्गोरिथ्म में अंतिम पुनरावृत्ति निर्देश में टेल पुनरावृत्ति पुनरावर्ती कॉल को अंतिम रूप से संदर्भित करता है।

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

factorial(x, fac=1) {
  if (x == 1)
     return fac;
   else
     return factorial(x-1, x*fac);
}

फैक्टरियल के लिए प्रारंभिक कॉल वह होगा factorial(n)जहां fac=1(डिफ़ॉल्ट मान) और n वह संख्या है जिसके लिए फैक्टरियल की गणना की जानी है।


मुझे आपका स्पष्टीकरण समझने में सबसे आसान लगा, लेकिन अगर यह कुछ भी करना है, तो पूंछ पुनरावृत्ति केवल एक बयान बेस मामलों वाले कार्यों के लिए उपयोगी है। इस postimg.cc/5Yg3Cdjn जैसी विधि पर विचार करें । नोट: बाहरी elseवह चरण है जिसे आप "बेस केस" कह सकते हैं, लेकिन कई लाइनों में फैला हुआ है। क्या मैं आपको गलत समझ रहा हूं या मेरी धारणा सही है? पूंछ पुनरावृत्ति केवल एक लाइनर के लिए अच्छा है?
मैं चाहता हूं कि उत्तर

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

28

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

मैंने इस विषय पर एक ब्लॉग पोस्ट लिखी है , जिसमें स्टैक फ्रेम की तरह दिखने वाले चित्रमय उदाहरण हैं।


21

यहां दो कार्यों की तुलना एक त्वरित कोड स्निपेट है। किसी दी गई संख्या के भाज्य को खोजने के लिए पहली पारंपरिक पुनरावृत्ति है। दूसरा पूंछ पुनरावृत्ति का उपयोग करता है।

समझने में बहुत सरल और सहज।

यह बताने का एक आसान तरीका है कि क्या एक पुनरावर्ती कार्य एक पूंछ पुनरावर्ती है यदि यह आधार मामले में एक ठोस मूल्य लौटाता है। मतलब यह है कि यह 1 या सही या ऐसा कुछ भी वापस नहीं करता है। यह विधि मापदंडों में से किसी एक के कुछ प्रकार की वापसी की संभावना से अधिक होगा।

एक और तरीका यह बताना है कि यदि पुनरावर्ती कॉल किसी भी अतिरिक्त, अंकगणित, संशोधन आदि से मुक्त है ... तो इसका एक शुद्ध पुनरावर्ती कॉल के अलावा कुछ भी नहीं है।

public static int factorial(int mynumber) {
    if (mynumber == 1) {
        return 1;
    } else {            
        return mynumber * factorial(--mynumber);
    }
}

public static int tail_factorial(int mynumber, int sofar) {
    if (mynumber == 1) {
        return sofar;
    } else {
        return tail_factorial(--mynumber, sofar * mynumber);
    }
}

3
0! है 1. तो "mynumber == 1" होना चाहिए "mynumber == 0"।
पोलो

19

मेरे लिए समझने tail call recursionका सबसे अच्छा तरीका एक विशेष मामले की पुनरावृत्ति है जहां अंतिम कॉल (या टेल कॉल) ही कार्य है।

पायथन में दिए गए उदाहरणों की तुलना:

def recsum(x):
 if x == 1:
  return x
 else:
  return x + recsum(x - 1)

^ प्रत्यावर्तन

def tailrecsum(x, running_total=0):
  if x == 0:
    return running_total
  else:
    return tailrecsum(x - 1, running_total + x)

^ जेल का दौरा

जैसा कि आप सामान्य पुनरावर्ती संस्करण में देख सकते हैं, कोड ब्लॉक में अंतिम कॉल है x + recsum(x - 1)। इसलिए recsumविधि को कॉल करने के बाद , एक और ऑपरेशन है जो है x + ..

हालांकि, पूंछ पुनरावर्ती संस्करण में, कोड ब्लॉक में अंतिम कॉल (या टेल कॉल) का tailrecsum(x - 1, running_total + x)मतलब है कि अंतिम कॉल स्वयं विधि और उसके बाद कोई ऑपरेशन नहीं किया जाता है।

यह बिंदु महत्वपूर्ण है क्योंकि यहां देखा गया पूंछ पुनरावृत्ति स्मृति को विकसित नहीं कर रहा है क्योंकि जब अंतर्निहित वीएम एक फ़ंक्शन को पूंछ की स्थिति में खुद को बुलाता हुआ देखता है (एक फ़ंक्शन में अंतिम अभिव्यक्ति का मूल्यांकन किया जाता है), तो यह वर्तमान स्टैक फ्रेम को समाप्त करता है, जो टेल कॉल ऑप्टिमाइज़ेशन (TCO) के रूप में जाना जाता है।

संपादित करें

एनबी। ध्यान रखें कि उपरोक्त उदाहरण पायथन में लिखा गया है जिसका रनटाइम TCO का समर्थन नहीं करता है। यह बात समझाने के लिए सिर्फ एक उदाहरण है। TCO को स्कीम, हास्केल आदि भाषाओं में सपोर्ट किया जाता है


12

जावा में, यहां फाइबोनैचि फ़ंक्शन का एक संभावित पूंछ पुनरावर्ती कार्यान्वयन है:

public int tailRecursive(final int n) {
    if (n <= 2)
        return 1;
    return tailRecursiveAux(n, 1, 1);
}

private int tailRecursiveAux(int n, int iter, int acc) {
    if (iter == n)
        return acc;
    return tailRecursiveAux(n, ++iter, acc + iter);
}

मानक पुनरावर्ती कार्यान्वयन के साथ इसका विरोध करें:

public int recursive(final int n) {
    if (n <= 2)
        return 1;
    return recursive(n - 1) + recursive(n - 2);
}

1
यह मेरे लिए गलत परिणाम दे रहा है, इनपुट 8 के लिए मुझे 36 मिलता है, इसे 21 होना चाहिए। क्या मुझे कुछ याद आ रहा है? मैं जावा और कॉपी पेस्ट कर रहा हूँ।
अल्बर्टो ज़काग्नि

1
यह SUM (i) को i [1, n] में लौटाता है। Fibbonacci के साथ कुछ नहीं करना है। एक फ़िबो के लिए, आपको एक परीक्षण की आवश्यकता होती है जो कब के iterलिए घट accजाती है iter < (n-1)
Askolein

10

मैं एक लिस्प प्रोग्रामर नहीं हूं, लेकिन मुझे लगता है कि इससे मदद मिलेगी।

मूल रूप से यह प्रोग्रामिंग की एक शैली है जैसे कि पुनरावर्ती कॉल आपके द्वारा किया जाने वाला अंतिम कार्य है।


10

यहां एक आम लिस्प उदाहरण है जो टेल-रिकर्सन का उपयोग करके फैक्टरियल करता है। स्टैक-कम प्रकृति के कारण, कोई भी व्यक्ति बड़ी तथ्यात्मक संगणना कर सकता है ...

(defun ! (n &optional (product 1))
    (if (zerop n) product
        (! (1- n) (* product n))))

और फिर मज़े के लिए आप कोशिश कर सकते थे (format nil "~R" (! 25))


9

संक्षेप में, एक पूंछ पुनरावर्ती के पास फ़ंक्शन में अंतिम विवरण के रूप में पुनरावर्ती कॉल है ताकि उसे पुनरावर्ती कॉल के लिए इंतजार न करना पड़े।

तो यह एक पूंछ पुनरावृत्ति है अर्थात एन (एक्स - 1, पी * एक्स) फ़ंक्शन का अंतिम विवरण है जहां संकलक यह पता लगाने के लिए चतुर है कि इसे फॉर-लूप (फैक्टरियल) के लिए अनुकूलित किया जा सकता है। दूसरा पैरामीटर p मध्यवर्ती उत्पाद मान को वहन करता है।

function N(x, p) {
   return x == 1 ? p : N(x - 1, p * x);
}

यह उपरोक्त फैक्टरियल फ़ंक्शन लिखने का गैर-पूंछ-पुनरावर्ती तरीका है (हालांकि कुछ सी ++ संकलक वैसे भी अनुकूलन करने में सक्षम हो सकते हैं)।

function N(x) {
   return x == 1 ? 1 : x * N(x - 1);
}

लेकिन यह नहीं है:

function F(x) {
  if (x == 1) return 0;
  if (x == 2) return 1;
  return F(x - 1) + F(x - 2);
}

मैंने " अंडरस्टैंडिंग टेल रिकर्सन - विजुअल स्टूडियो सी ++ - असेंबली व्यू " शीर्षक से एक लंबी पोस्ट लिखी थी

यहां छवि विवरण दर्ज करें


1
आपका कार्य N पूंछ-पुनरावर्ती कैसे है?
फेबियन पीजेक

N (x-1) फ़ंक्शन का अंतिम कथन है जहां संकलक यह पता लगाने के लिए चतुर है कि इसे फॉर-लूप (फैक्टरियल) के लिए अनुकूलित किया जा सकता है
डॉक्टरीलाई

मेरी चिंता यह है कि आपका फंक्शन N वास्तव में इस विषय के स्वीकृत उत्तर से फंक्शन रिकसम है (सिवाय इसके कि यह एक योग और एक उत्पाद नहीं है), और उस रिकसम को नॉन टेल-रिकर्सिव कहा जाता है?
फेबियन पिज्के 12

8

यहाँ tailrecsumपहले उल्लिखित फ़ंक्शन का एक पर्ल 5 संस्करण है ।

sub tail_rec_sum($;$){
  my( $x,$running_total ) = (@_,0);

  return $running_total unless $x;

  @_ = ($x-1,$running_total+$x);
  goto &tail_rec_sum; # throw away current stack frame
}

8

यह पूंछ पुनरावृत्ति के बारे में कंप्यूटर प्रोग्राम की संरचना और व्याख्या से एक अंश है ।

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

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


1
मैं यहां सभी उत्तरों के माध्यम से पढ़ता हूं, और फिर भी यह सबसे स्पष्ट स्पष्टीकरण है जो इस अवधारणा के वास्तव में गहरे मूल को छूता है। यह इसे ऐसे सीधे तरीके से समझाता है जिससे सब कुछ इतना सरल और स्पष्ट हो जाता है। कृपया मेरी अशिष्टता को क्षमा करें। यह किसी भी तरह मुझे अन्य उत्तरों की तरह महसूस करता है जैसे कि सिर पर कील नहीं मारते। मुझे लगता है कि इसीलिए SICP मायने रखता है।
20

8

पुनरावर्ती फ़ंक्शन एक फ़ंक्शन है जो स्वयं द्वारा कॉल करता है

यह प्रोग्रामर को कम से कम कोड का उपयोग करके कुशल प्रोग्राम लिखने की अनुमति देता है ।

नकारात्मक पक्ष यह है कि वे ठीक से नहीं लिखे जाने पर अनंत छोरों और अन्य अप्रत्याशित परिणामों का कारण बन सकते हैं

मैं Simple Recursive function और Tail Recursive function दोनों की व्याख्या करूँगा

एक साधारण पुनरावर्ती कार्य लिखने के लिए

  1. विचार करने के लिए पहला बिंदु यह है कि आपको लूप से बाहर आने का फैसला करना चाहिए जो अगर लूप है
  2. दूसरा यह है कि अगर हम अपने स्वयं के कार्य करते हैं तो क्या करना है

दिए गए उदाहरण से:

public static int fact(int n){
  if(n <=1)
     return 1;
  else 
     return n * fact(n-1);
}

उपरोक्त उदाहरण से

if(n <=1)
     return 1;

पाश से बाहर निकलने के लिए निर्णायक कारक है

else 
     return n * fact(n-1);

क्या वास्तविक प्रसंस्करण किया जाना है

मुझे आसानी से समझने के लिए एक-एक करके टास्क को तोड़ना चाहिए।

आइए देखते हैं कि अगर मैं दौड़ता हूं तो आंतरिक रूप से क्या होता है fact(4)

  1. उपादान n = 4
public static int fact(4){
  if(4 <=1)
     return 1;
  else 
     return 4 * fact(4-1);
}

Ifलूप विफल हो जाता है elseइसलिए यह लूप में जाता है इसलिए यह वापस आ जाता है4 * fact(3)

  1. स्टैक मेमोरी में, हमारे पास है 4 * fact(3)

    उपसर्ग करना = ३

public static int fact(3){
  if(3 <=1)
     return 1;
  else 
     return 3 * fact(3-1);
}

Ifलूप विफल हो जाता है इसलिए यह elseलूप में जाता है

तो यह लौट आता है 3 * fact(2)

याद रखें हम `` `4 * तथ्य (3)` `कहते हैं

के लिए उत्पादन fact(3) = 3 * fact(2)

अब तक ढेर है 4 * fact(3) = 4 * 3 * fact(2)

  1. स्टैक मेमोरी में, हमारे पास है 4 * 3 * fact(2)

    प्रतिस्थापित करना = २

public static int fact(2){
  if(2 <=1)
     return 1;
  else 
     return 2 * fact(2-1);
}

Ifलूप विफल हो जाता है इसलिए यह elseलूप में जाता है

तो यह लौट आता है 2 * fact(1)

याद है हमने बुलाया 4 * 3 * fact(2)

के लिए उत्पादन fact(2) = 2 * fact(1)

अब तक ढेर है 4 * 3 * fact(2) = 4 * 3 * 2 * fact(1)

  1. स्टैक मेमोरी में, हमारे पास है 4 * 3 * 2 * fact(1)

    प्रतिस्थापित करना = १

public static int fact(1){
  if(1 <=1)
     return 1;
  else 
     return 1 * fact(1-1);
}

If पाश सत्य है

तो यह लौट आता है 1

याद है हमने बुलाया 4 * 3 * 2 * fact(1)

के लिए उत्पादन fact(1) = 1

अब तक ढेर है 4 * 3 * 2 * fact(1) = 4 * 3 * 2 * 1

अंत में, तथ्य (4) = 4 * 3 * 2 * 1 = 24 का परिणाम

यहां छवि विवरण दर्ज करें

पूंछ Recursion होगा

public static int fact(x, running_total=1) {
    if (x==1) {
        return running_total;
    } else {
        return fact(x-1, running_total*x);
    }
}

  1. उपादान n = 4
public static int fact(4, running_total=1) {
    if (x==1) {
        return running_total;
    } else {
        return fact(4-1, running_total*4);
    }
}

Ifलूप विफल हो जाता है elseइसलिए यह लूप में जाता है इसलिए यह वापस आ जाता हैfact(3, 4)

  1. स्टैक मेमोरी में, हमारे पास है fact(3, 4)

    उपसर्ग करना = ३

public static int fact(3, running_total=4) {
    if (x==1) {
        return running_total;
    } else {
        return fact(3-1, 4*3);
    }
}

Ifलूप विफल हो जाता है इसलिए यह elseलूप में जाता है

तो यह लौट आता है fact(2, 12)

  1. स्टैक मेमोरी में, हमारे पास है fact(2, 12)

    प्रतिस्थापित करना = २

public static int fact(2, running_total=12) {
    if (x==1) {
        return running_total;
    } else {
        return fact(2-1, 12*2);
    }
}

Ifलूप विफल हो जाता है इसलिए यह elseलूप में जाता है

तो यह लौट आता है fact(1, 24)

  1. स्टैक मेमोरी में, हमारे पास है fact(1, 24)

    प्रतिस्थापित करना = १

public static int fact(1, running_total=24) {
    if (x==1) {
        return running_total;
    } else {
        return fact(1-1, 24*1);
    }
}

If पाश सत्य है

तो यह लौट आता है running_total

के लिए उत्पादन running_total = 24

अंत में, तथ्य (4,1) = 24 का परिणाम

यहां छवि विवरण दर्ज करें


7

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

जब आप कुछ प्रक्रियाओं को अतिरिक्त फ्रेम का उपयोग कर सकते हैं, तब भी सादृश्य टूट जाता है, लेकिन अभी भी पूंछ-पुनरावर्ती माना जाता है यदि स्टैक असीम रूप से नहीं बढ़ता है।


1
यह विभाजन व्यक्तित्व विकार व्याख्या के तहत नहीं टूटता है । :) ए सोसाइटी ऑफ़ माइंड; एक समाज के रूप में एक मन। :)
विल नेस

वाह! अब इसके बारे में सोचने का एक और तरीका है
sutanu dalui

7

एक पूंछ पुनरावर्तन एक पुनरावर्ती कार्य है जहां फ़ंक्शन स्वयं को फ़ंक्शन के अंत ("पूंछ") पर बुलाता है जिसमें पुनरावर्ती कॉल की वापसी के बाद कोई गणना नहीं की जाती है। कई संकलक एक पुनरावर्ती कॉल को पुनरावर्ती या पुनरावृत्ति कॉल में बदलने का अनुकूलन करते हैं।

किसी संख्या के कंप्यूटिंग तथ्य की समस्या पर विचार करें।

एक सीधा दृष्टिकोण होगा:

  factorial(n):

    if n==0 then 1

    else n*factorial(n-1)

मान लीजिए आप तथ्यात्मक (4) कहते हैं। पुनरावर्तन वृक्ष होगा:

       factorial(4)
       /        \
      4      factorial(3)
     /             \
    3          factorial(2)
   /                  \
  2                factorial(1)
 /                       \
1                       factorial(0)
                            \
                             1    

उपरोक्त मामले में अधिकतम पुनरावृत्ति की गहराई O (n) है।

हालांकि, निम्नलिखित उदाहरण पर विचार करें:

factAux(m,n):
if n==0  then m;
else     factAux(m*n,n-1);

factTail(n):
   return factAux(1,n);

तथ्य के लिए पुनरावर्तन पेड़ (4) होगा:

factTail(4)
   |
factAux(1,4)
   |
factAux(4,3)
   |
factAux(12,2)
   |
factAux(24,1)
   |
factAux(24,0)
   |
  24

यहां भी, अधिकतम पुनरावृत्ति गहराई O (n) है, लेकिन कोई भी कॉल स्टैक में कोई अतिरिक्त चर नहीं जोड़ता है। इसलिए संकलक एक स्टैक के साथ दूर कर सकता है।


7

सामान्य पुनरावृत्ति की तुलना में पूंछ पुनरावृत्ति बहुत तेज है। यह तेज़ है क्योंकि ट्रैक रखने के लिए पूर्वजों के कॉल का आउटपुट स्टैक में नहीं लिखा जाएगा। लेकिन सामान्य पुनरावृत्ति में सभी पूर्वज ट्रैक को बनाए रखने के लिए स्टैक में लिखा आउटपुट कहते हैं।


6

एक पूंछ पुनरावर्ती कार्य एक पुनरावर्ती कार्य है जहां लौटने से पहले यह अंतिम ऑपरेशन करता है जो पुनरावर्ती फ़ंक्शन कॉल करता है। यही है, पुनरावर्ती फ़ंक्शन कॉल का वापसी मूल्य तुरंत वापस आ जाता है। उदाहरण के लिए, आपका कोड इस तरह दिखेगा:

def recursiveFunction(some_params):
    # some code here
    return recursiveFunction(some_args)
    # no code after the return statement

कंपाइलर और दुभाषिए जो टेल कॉल ऑप्टिमाइज़ेशन या टेल कॉल एलिमिनेशन को लागू करते हैं, स्टैक ओवरफ्लो को रोकने के लिए पुनरावर्ती कोड का अनुकूलन कर सकते हैं। यदि आपका कंपाइलर या दुभाषिया टेल कॉल ऑप्टिमाइजेशन (जैसे कि CPython दुभाषिया) को लागू नहीं करता है तो आपके कोड को इस तरह लिखने का कोई अतिरिक्त लाभ नहीं है।

उदाहरण के लिए, यह पायथन में एक मानक पुनरावर्ती फैक्टरियल फ़ंक्शन है:

def factorial(number):
    if number == 1:
        # BASE CASE
        return 1
    else:
        # RECURSIVE CASE
        # Note that `number *` happens *after* the recursive call.
        # This means that this is *not* tail call recursion.
        return number * factorial(number - 1)

और यह गुटीय कार्य के एक पुनरावर्ती संस्करण है।

def factorial(number, accumulator=1):
    if number == 0:
        # BASE CASE
        return accumulator
    else:
        # RECURSIVE CASE
        # There's no code after the recursive call.
        # This is tail call recursion:
        return factorial(number - 1, number * accumulator)
print(factorial(5))

(ध्यान दें कि भले ही यह पायथन कोड हो, लेकिन CPython दुभाषिया टेल कॉल ऑप्टिमाइज़ेशन नहीं करता है, इसलिए आपके कोड को इस तरह व्यवस्थित करना कोई रनटाइम लाभ प्रदान नहीं करता है।)

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

लेकिन टेल कॉल ऑप्टिमाइजेशन का लाभ यह है कि यह स्टैक ओवरफ्लो त्रुटियों को रोकता है। (मैं ध्यान देता हूँ कि आप पुनरावर्ती के बजाय पुनरावृत्त एल्गोरिथ्म का उपयोग करके यह लाभ प्राप्त कर सकते हैं।)

स्टैक ओवरफ्लो तब होता है जब कॉल स्टैक में बहुत सारे फ्रेम ऑब्जेक्ट्स को धक्का दिया जाता है। किसी फ़ंक्शन को कॉल करने पर फ़्रेम ऑब्जेक्ट को कॉल स्टैक पर धकेल दिया जाता है, और फ़ंक्शन वापस आने पर कॉल स्टैक को पॉपअप किया जाता है। फ़्रेम ऑब्जेक्ट में स्थानीय चर जैसी जानकारी होती है और फ़ंक्शन लौटने पर कोड की किस पंक्ति को वापस करना है।

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

हालाँकि, यदि आपके पुनरावर्ती कार्य की अंतिम चीज़ पुनरावर्ती कॉल कर रही है और उसका रिटर्न वैल्यू लौटाती है, तो इसका कोई कारण नहीं है कि कॉल फ्रेम पर रहने के लिए वर्तमान फ़्रेम ऑब्जेक्ट को रखने की आवश्यकता है। आखिरकार, यदि पुनरावर्ती फ़ंक्शन कॉल के बाद कोई कोड नहीं है, तो वर्तमान फ़्रेम ऑब्जेक्ट के स्थानीय चर पर लटका देने का कोई कारण नहीं है। इसलिए हम कॉल फ्रेम पर रखने के बजाय तुरंत वर्तमान फ्रेम ऑब्जेक्ट से छुटकारा पा सकते हैं। इसका अंतिम परिणाम यह है कि आपका कॉल स्टैक आकार में नहीं बढ़ता है, और इस प्रकार अतिप्रवाह नहीं हो सकता है।

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


"पूंछ पुनरावृत्ति (जिसे टेल कॉल ऑप्टिमाइज़ेशन या टेल कॉल एलिमिनेशन भी कहा जाता है)"। नहीं; टेल-कॉल एलिमिनेशन या टेल-कॉल ऑप्टिमाइज़ेशन एक ऐसी चीज है, जिसे आप टेल-रिकर्सिव फ़ंक्शन पर लागू कर सकते हैं , लेकिन वे एक ही चीज़ नहीं हैं। आप पायथन में पूंछ-पुनरावर्ती कार्य लिख सकते हैं (जैसा कि आप दिखाते हैं), लेकिन वे गैर-पूंछ-पुनरावर्ती फ़ंक्शन की तुलना में अधिक कुशल नहीं हैं, क्योंकि पायथन पूंछ-कॉल अनुकूलन का प्रदर्शन नहीं करता है।
चेपर

क्या इसका मतलब यह है कि अगर कोई वेबसाइट का अनुकूलन करता है और पुनरावर्ती कॉल पूंछ-पुनरावर्ती को प्रस्तुत करता है तो हमारे पास अब StackOverflow साइट नहीं होगी ?! यह खराब है।
नादजीब ममी

5

पूंछ-कॉल पुनरावृत्ति और गैर-पूंछ-कॉल पुनरावृत्ति के बीच कुछ मुख्य अंतरों को समझने के लिए हम इन तकनीकों के .NET कार्यान्वयन का पता लगा सकते हैं।

यहाँ C #, F #, और C ++ \ CLI में कुछ उदाहरणों के साथ एक लेख दिया गया है: C #, F #, और C ++ \ CLI में टेल रिसर्शन में एडवेंचर्स

C # टेल-कॉल रिकर्सन के लिए ऑप्टिमाइज़ नहीं करता है जबकि F # करता है।

सिद्धांत के अंतरों में लूप्स बनाम लैंबडा कैलकुलस शामिल है। C # को लूप्स को ध्यान में रखते हुए बनाया गया है जबकि F # को लैम्ब्डा कैलकुलस के सिद्धांतों से बनाया गया है। लैंबडा कैलकुलस के सिद्धांतों पर एक बहुत अच्छी (और मुफ्त) पुस्तक के लिए , अबेल्सन, सुस्मान और सुस्मान द्वारा कंप्यूटर प्रोग्राम की संरचना और व्याख्या देखें ।

बहुत अच्छे परिचयात्मक लेख के लिए एफ # में टेल कॉल के बारे में, एफ # में टेल कॉल का विस्तृत परिचय देखें । अंत में, यहां एक लेख है जो गैर-पूंछ पुनरावृत्ति और पूंछ-कॉल पुनरावृत्ति (एफ # में) के बीच अंतर को कवर करता है: एफ तेज में पूंछ-पुनरावृत्ति बनाम गैर-पूंछ पुनरावृत्ति

यदि आप C # और F # के बीच टेल-कॉल पुनरावृत्ति के कुछ डिज़ाइन अंतर के बारे में पढ़ना चाहते हैं, तो C # और F # में Generate Tail-Call Opcode देखें ।

यदि आप यह जानना चाहते हैं कि सी # कंपाइलर को टेल-कॉल ऑप्टिमाइज़ेशन करने से रोकने के लिए कौन सी स्थितियाँ जानना चाहते हैं, तो इस लेख को देखें: JIT CLR टेल-कॉल की स्थिति


4

दो प्रकार की पुनरावर्ती हैं: सिर की पुनरावृत्ति और पूंछ की पुनरावृत्ति।

में सिर प्रत्यावर्तन , एक समारोह में अपनी पुनरावर्ती कॉल करता है और फिर कुछ और गणना करता है, हो सकता है पुनरावर्ती कॉल का परिणाम का उपयोग कर, उदाहरण के लिए।

एक पूंछ पुनरावर्ती कार्य में, सभी गणना पहले होती हैं और पुनरावर्ती कॉल अंतिम चीज होती है।

इस सुपर भयानक पोस्ट से लिया गया । कृपया इसे पढ़ने पर विचार करें।


4

रिकर्सियन का अर्थ है एक फ़ंक्शन जो स्वयं को बुला रहा है। उदाहरण के लिए:

(define (un-ended name)
  (un-ended 'me)
  (print "How can I get here?"))

टेल-पुनर्संरचना का अर्थ है कि कार्य समाप्त करने वाली पुनरावृत्ति:

(define (un-ended name)
  (print "hello")
  (un-ended 'me))

देखें, अंतिम कार्य अन-एंडेड फ़ंक्शन (कार्यविधि, स्कीम शब्दजाल) में ही कॉल करना है। एक और (अधिक उपयोगी) उदाहरण है:

(define (map lst op)
  (define (helper done left)
    (if (nil? left)
        done
        (helper (cons (op (car left))
                      done)
                (cdr left))))
  (reverse (helper '() lst)))

सहायक प्रक्रिया में, यदि बाईं ओर शून्य नहीं है, तो सबसे बड़ी बात यह है कि खुद को बुलाना है (कुछ को ठीक करें और कुछ को हटाएं)। यह मूल रूप से है कि आप किसी सूची को कैसे मैप करते हैं।

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


3

इस सवाल के बहुत सारे शानदार उत्तर हैं ... लेकिन मैं मदद नहीं कर सकता, लेकिन एक विकल्प के साथ झंकार कर सकता हूं कि "पूंछ पुनरावृत्ति" को कैसे परिभाषित किया जाए, या कम से कम "उचित पूंछ पुनरावृत्ति।" अर्थात्: एक कार्यक्रम में एक विशेष अभिव्यक्ति की संपत्ति के रूप में इसे देखना चाहिए? या किसी प्रोग्रामिंग भाषा के कार्यान्वयन के गुण के रूप में इसे देखना चाहिए ?

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

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

कागज कई कारणों से सावधान अध्ययन के लायक है:

  • यह एक प्रोग्राम की टेल एक्सप्रेशंस और टेल कॉल की इंडक्टिव परिभाषा देता है । (इस तरह की एक परिभाषा, और इस तरह के कॉल क्यों महत्वपूर्ण हैं, यहां दिए गए अधिकांश अन्य उत्तरों का विषय लगता है।)

    यहां वे परिभाषाएं दी गई हैं, बस पाठ का एक स्वाद प्रदान करने के लिए:

    परिभाषा 1 पूंछ भाव कोर योजना में लिखा गया प्रोग्राम के रूप में परिभाषित कर रहे हैं उपपादन द्वारा इस प्रकार है।

    1. लंबोदर अभिव्यक्ति का शरीर एक पूंछ अभिव्यक्ति है
    2. यदि (if E0 E1 E2)एक पूंछ अभिव्यक्ति है, तो दोनों E1और E2पूंछ अभिव्यक्तियां हैं।
    3. और कुछ नहीं एक पूंछ अभिव्यक्ति है।

    परिभाषा 2 एक टेल कॉल एक पूंछ अभिव्यक्ति है जो एक प्रक्रिया कॉल है।

(एक पूंछ पुनरावर्ती कॉल, या जैसा कि कागज़ कहता है, "सेल्फ-टेल कॉल" एक टेल कॉल का एक विशेष मामला है, जहाँ प्रक्रिया को स्वयं लागू किया जाता है।)

  • यह कोर योजना है, जहां प्रत्येक मशीन एक ही नमूदार व्यवहार है मूल्यांकन के लिए छह अलग "मशीनों" के लिए औपचारिक परिभाषाओं प्रदान करता है को छोड़कर के लिए asymptotic अंतरिक्ष जटिलता वर्ग है कि प्रत्येक में है।

    उदाहरण के लिए, क्रमशः मशीनों के लिए परिभाषा देने के बाद, 1. स्टैक-आधारित मेमोरी प्रबंधन, 2. कचरा संग्रह लेकिन कोई पूंछ कॉल, 3. कचरा संग्रह और पूंछ कॉल, कागज आगे भी उन्नत भंडारण प्रबंधन रणनीतियों के साथ जारी है, जैसे कि 4. "evlis पूंछ प्रत्यावर्तन", जहां वातावरण एक पूंछ कॉल में पिछले उप अभिव्यक्ति तर्क के मूल्यांकन पर संरक्षित किया जाना है, 5. करने के लिए एक बंद करने के वातावरण को कम करने की जरूरत नहीं है बस कि बंद से मुक्त चर, और 6. तथाकथित "सुरक्षित-फॉर-स्पेस" शब्दार्थ द्वारा परिभाषित एपेल और शाओ

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


(मेरे जवाब पर अब पढ़ना, मुझे यकीन नहीं है कि अगर मैं वास्तव में क्लिंजर पेपर के महत्वपूर्ण बिंदुओं पर कब्जा करने में कामयाब रहा हूं । लेकिन, अफसोस, मैं अभी इस उत्तर को विकसित करने के लिए अधिक समय नहीं दे सकता हूं।)


1

बहुत से लोग पहले ही यहाँ पुनरावृत्ति के बारे में बता चुके हैं। मैं कुछ फायदों के बारे में कुछ विचारों का हवाला देना चाहूंगा जो रिकर्सार्डो टेरेल की पुस्तक "कॉन्सेप्ट इन .NET, समवर्ती और समानांतर प्रोग्रामिंग के आधुनिक पैटर्न" से देता है।

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

यहाँ भी पूंछ पुनरावृत्ति के बारे में एक ही किताब से कुछ दिलचस्प नोट हैं:

टेल-कॉल पुनरावृत्ति एक ऐसी तकनीक है जो एक नियमित पुनरावर्ती कार्य को एक अनुकूलित संस्करण में परिवर्तित करती है जो बिना किसी जोखिम और दुष्प्रभावों के बड़े इनपुट को संभाल सकती है।

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

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