पुनरावर्तन का उपयोग कब करें?


26

जब कुछ (अपेक्षाकृत) बुनियादी (प्रथम वर्ष कॉलेज स्तर के सीएस छात्र सोचते हैं) उदाहरण हैं जब कोई सिर्फ लूप के बजाय पुनरावृत्ति का उपयोग करेगा?


2
आप किसी भी पुनरावृत्ति को लूप (स्टैक के साथ) में बदल सकते हैं।
केवह

जवाबों:


19

मैंने C ++ को लगभग दो साल तक और अंडरस्क्रिमेशन को कवर करने के लिए पढ़ाया है। मेरे अनुभव से, आपका प्रश्न और भावनाएँ बहुत सामान्य हैं। एक चरम पर, कुछ छात्र पुनरावृत्ति को समझने में मुश्किल होते हैं जबकि अन्य इसे बहुत अधिक हर चीज के लिए उपयोग करना चाहते हैं।

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

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

क्या एक स्ट्रिंग में एक वर्ण ?x

यह हमने पहले कैसे किया था: स्ट्रिंग को पुनरावृत्त करें, और देखें कि क्या किसी सूचकांक में x

bool find(const std::string& s, char x)
{
   for(int i = 0; i < s.size(); ++i)
   {
      if(s[i] == x)
         return true;
   }

   return false;
}

सवाल यह है कि क्या हम इसे पुनरावृत्ति कर सकते हैं ? ज़रूर हम कर सकते हैं, यहाँ एक तरीका है:

bool find(const std::string& s, int idx, char x)
{
   if(idx == s.size())
      return false;

   return s[idx] == x || find(s, ++idx);
}

अगला प्राकृतिक सवाल यह है कि क्या हमें ऐसा करना चाहिए? शायद ऩही। क्यूं कर? यह समझना कठिन है और साथ आना कठिन है। इसलिए इसमें त्रुटि होने की संभावना अधिक है।


2
अंतिम पैराग्राफ गलत नहीं है; बस इसका उल्लेख करना चाहते हैं, एक ही तर्क पुनरावृत्ति समाधान (Quicksort!) पर पुनरावर्ती के पक्ष में है।
राफेल

1
@ राफेल सहमत, बिल्कुल। कुछ चीजें पुनरावृत्त रूप से व्यक्त करने के लिए अधिक स्वाभाविक हैं, अन्य पुनरावर्ती हैं। यही वह बिंदु था जिसे मैं बनाने की कोशिश कर रहा था :)
जुहो

उम्म, मुझे माफ कर दो अगर मैं गलत हूं, लेकिन बेहतर नहीं होगा यदि आपने रिटर्न कोड को उदाहरण कोड में एक शर्त में अलग कर दिया है, जो कि एक्स पाए जाने पर वापस लौटता है, तो पुनरावर्ती भाग? मुझे नहीं पता कि क्या 'या' सच होने पर भी निष्पादित होना जारी है, लेकिन यदि ऐसा है, तो यह कोड अत्यधिक अक्षम है।
माइंडलेसरंगर

@MindlessRanger शायद एक आदर्श उदाहरण है कि पुनरावर्ती संस्करण को समझना और लिखना कठिन है? :-)
जुहो

हाँ, और मेरी पिछली टिप्पणी गलत थी, 'या' या '|| अगले शर्तों की जांच नहीं करता है तो पहली शर्त सही है, तो वहाँ कोई ineffiency है
MindlessRanger

24

कुछ समस्याओं का समाधान पुनरावृत्ति का उपयोग करके अधिक स्वाभाविक रूप से व्यक्त किया जाता है।

उदाहरण के लिए, मान लें कि आपके पास दो प्रकार के नोड्स के साथ एक पेड़ डेटा संरचना है: पत्तियां, जो एक पूर्णांक मान संग्रहीत करती हैं; और शाखाएँ, जिनके खेतों में बाएँ और दाएँ सबट्री हैं। मान लें कि पत्तियों का आदेश दिया गया है, ताकि सबसे कम मूल्य बाईं पत्ती में हो।

मान लीजिए कि कार्य क्रम में पेड़ के मूल्यों को प्रिंट करना है। ऐसा करने के लिए एक पुनरावर्ती एल्गोरिथ्म काफी स्वाभाविक है:

class Node { abstract void traverse(); }
class Leaf extends Node { 
  int val; 
  void traverse() { print(val); }
} 
class Branch extends Node {
  Node left, right;
  void traverse() { left.traverse(); right.traverse(); }
}

पुनरावृत्ति के बिना समतुल्य कोड लिखना अधिक कठिन होगा। कोशिश करो!

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

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

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


1
क्या C ++ में पूंछ पुनरावृत्ति है? यह इंगित करने योग्य हो सकता है कि कार्यात्मक भाषाएं आमतौर पर करती हैं।
लुई

3
धन्यवाद लुई। कुछ C ++ कंपाइलर टेल कॉल को ऑप्टिमाइज़ करते हैं। (टेल रिकर्सियन प्रोग्राम की एक संपत्ति है, भाषा की नहीं।) मैंने अपना उत्तर अपडेट किया।
डेव क्लार्क

कम से कम जीसीसी टेल कॉल (और यहां तक ​​कि गैर-टेल कॉल के कुछ रूप) का अनुकूलन करता है।
वॉनब्रांड

11

किसी से जो व्यावहारिक रूप से पुनरावृत्ति में रहता है, मैं इस विषय पर कुछ प्रकाश डालने की कोशिश करूंगा।

जब पहली बार पुनरावृत्ति के लिए पेश किया जाता है, तो आप सीखते हैं कि यह एक ऐसा कार्य है जो स्वयं को कॉल करता है और मूल रूप से एल्गोरिदम जैसे कि पेड़ ट्रैवर्सल के साथ प्रदर्शित किया जाता है। बाद में आपको पता चलता है कि यह LISP और F # जैसी भाषाओं के लिए कार्यात्मक प्रोग्रामिंग में बहुत उपयोग किया जाता है । एफ # के साथ, मैं जो लिखता हूं उसका अधिकांश पुनरावर्ती और पैटर्न मिलान है।

यदि आप F # जैसे कार्यात्मक प्रोग्रामिंग के बारे में अधिक जानते हैं, तो आप सीखेंगे F # सूचियों को एकल रूप से लिंक की गई सूचियों के रूप में कार्यान्वित किया जाता है, जिसका अर्थ है कि परिचालन जो केवल सूची के प्रमुख तक पहुंचते हैं वे हैं O (1), और तत्व की पहुंच O (n) है। एक बार जब आप इसे सीख लेते हैं तो आप डेटा को सूची में बदल देते हैं, नई सूची को रिवर्स ऑर्डर में बनाते हैं और फिर फ़ंक्शन से लौटने से पहले सूची को उलट देते हैं जो बहुत प्रभावी है।

अब अगर आप इस बारे में सोचना शुरू करते हैं तो आपको जल्द ही पता चल जाएगा कि रिकर्सिव फंक्शंस स्टैक फ्रेम को हर बार फंक्शन कॉल करने के लिए प्रेरित करेगा और स्टैक ओवरफ्लो का कारण बन सकता है। हालाँकि, यदि आप अपने पुनरावर्ती फ़ंक्शन का निर्माण करते हैं ताकि यह एक टेल कॉल कर सके और कंपाइलर टेल कॉल के लिए कोड को ऑप्टिमाइज़ करने की क्षमता का समर्थन करता है। यानी .NET OpCodes.Tailcall फ़ील्ड में आप स्टैक ओवरफ़्लो का कारण नहीं बनेंगे । इस बिंदु पर आप एक पुनरावर्ती फ़ंक्शन के रूप में किसी भी लूपिंग को लिखना शुरू करते हैं, और एक मैच के रूप में कोई भी निर्णय; के दिन हैं ifऔर whileअब इतिहास हैं।

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

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

यदि आप जेन स्ट्रीट को देखते हैं तो आप देखेंगे कि वे कार्यात्मक भाषा OCaml का उपयोग करते हैं । जबकि मैंने उनके किसी भी कोड को नहीं देखा है, पढ़ने से लेकर उनके कोड के बारे में जो कुछ भी उन्होंने उल्लेख किया है, वे सरसरी तौर पर पुनरावर्ती सोच रहे हैं।

संपादित करें

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

सी ++ के लिए: यदि आप एक संरचना या एक वर्ग को परिभाषित करते हैं जिसमें उसी संरचना या वर्ग के लिए एक संकेतक है तो पुनरावृत्ति को ट्रैवर्सल विधियों के लिए माना जाना चाहिए जो बिंदुओं का उपयोग करते हैं।

साधारण मामला एक तरह से लिंक की गई सूची है। आप सूची को सिर या पूँछ पर शुरू करने की प्रक्रिया करेंगे और फिर बिंदुओं का उपयोग करके सूची को पुनरावृत्ति करेंगे।

एक पेड़ एक और मामला है जहां अक्सर पुनरावृत्ति का उपयोग किया जाता है; इतना अधिक है कि यदि आप पुनरावृत्ति के बिना पेड़ की कटाई देखते हैं, तो आपको पूछना चाहिए कि क्यों? यह गलत नहीं है, लेकिन कुछ ऐसा है जिसे टिप्पणियों में नोट किया जाना चाहिए।

पुनरावर्तन के सामान्य उपयोग हैं:


2
यह वास्तव में एक महान जवाब की तरह लगता है, हालांकि यह कुछ भी थोड़ा ऊपर है जो मेरी कक्षाओं में सिखाया जा रहा है जल्द ही मुझे विश्वास है।
टेलर हस्टन

1
@TaylorHuston याद रखें कि आप ग्राहक हैं; शिक्षक से उन अवधारणाओं को पूछें जिन्हें आप समझना चाहते हैं। वह शायद कक्षा में उन्हें जवाब नहीं देगा, लेकिन कार्यालय के घंटों के दौरान उसे पकड़ सकता है और भविष्य में बहुत सारे लाभांश का भुगतान कर सकता है।
गाय कोडर

अच्छा जवाब है, लेकिन यह किसी ऐसे व्यक्ति के लिए बहुत उन्नत है जो कार्यात्मक प्रोग्रामिंग के बारे में नहीं जानता है :)।
पैड

2
... कार्यात्मक प्रोग्रामिंग का अध्ययन करने के लिए भोले प्रश्नकर्ता का नेतृत्व। जीत!
जेफ ईई

8

आपको एक उपयोग का मामला देने के लिए जो अन्य उत्तरों में दिए गए की तुलना में कम आर्कैन है: एक आम स्रोत से प्राप्त होने वाली वृक्ष जैसी (ऑब्जेक्ट ओरिएंटेड) वर्ग संरचनाओं के साथ पुनरावृत्ति बहुत अच्छी तरह से मिश्रण करती है। एक C ++ उदाहरण:

class Expression {
public:
    // The "= 0" means 'I don't implement this, I let my subclasses do that'
    virtual int ComputeValue() = 0;
}

class Plus : public Expression {
private:
    Expression* left
    Expression* right;
public:
    virtual int ComputeValue() { return left->ComputeValue() + right->ComputeValue(); }
}

class Times : public Expression {
private:
    Expression* left
    Expression* right;
public:
    virtual int ComputeValue() { return left->ComputeValue() * right->ComputeValue(); }
}

class Negate : public Expression {
private:
    Expression* expr;
public:
    virtual int ComputeValue() { return -(expr->ComputeValue()); }
}

class Constant : public Expression {
private:
    int value;
public:
    virtual int ComputeValue() { return value; }
}

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

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


1
मुझे यकीन नहीं है कि आप किस 'रहस्यमय' उदाहरण का जिक्र कर रहे हैं। फिर भी, OO के साथ एकीकरण की अच्छी चर्चा।
डेव क्लार्क

3

मेरी शुरुआत की प्रोग्रामिंग क्लास में पुनरावृत्ति सिखाने के लिए इस्तेमाल किया गया पहला उदाहरण एक क्रम में सभी अंकों को अलग-अलग क्रम में सूचीबद्ध करने के लिए एक फ़ंक्शन था।

void listDigits(int x){
     if (x <= 0)
        return;
     print x % 10;
     listDigits(x/10);
}

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

तो यह अब भाषा में एक बेकार कार्य की तरह लग सकता है, लेकिन यह लंबे समय में बहुत उपयोगी है।

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