पुनरावर्ती एल्गोरिदम में स्टैक ओवरफ्लो से बचने के लिए क्या तरीके हैं?


44

सवाल

पुनरावर्ती एल्गोरिथ्म के कारण होने वाले स्टैक ओवरफ्लो को हल करने के संभावित तरीके क्या हैं?

उदाहरण

मैं प्रोजेक्ट यूलर समस्या 14 को हल करने की कोशिश कर रहा हूं और इसे पुनरावर्ती एल्गोरिदम के साथ आजमाने का फैसला किया। हालाँकि, प्रोग्राम java.lang.StackOverflowError के साथ बंद हो जाता है। जाहिर है। एल्गोरिथ्म वास्तव में स्टैक से बह निकला क्योंकि मैंने बहुत बड़ी संख्या के लिए कोलेज़ अनुक्रम बनाने की कोशिश की थी।

समाधान

तो मैं सोच रहा था: आपके पुनरावर्ती एल्गोरिथ्म को सही ढंग से लिखे जाने पर एक स्टैक ओवरफ्लो को हल करने के लिए कौन से मानक तरीके हैं और हमेशा स्टैक ओवरफ्लो करना समाप्त होगा? दिमाग में आई दो अवधारणाएँ थीं:

  1. पूंछ की पुनरावृत्ति
  2. यात्रा

क्या विचार (1) और (2) सही हैं? क्या अन्य विकल्प हैं?

संपादित करें

यह जावा, C #, ग्रूवी या स्काला में कुछ कोड देखने में मदद करेगा।

शायद ऊपर उल्लिखित प्रोजेक्ट यूलर समस्या का उपयोग न करें ताकि यह दूसरों के लिए खराब न हो, लेकिन कुछ अन्य एल्गोरिथ्म लें। फैक्टरियल शायद, या कुछ इसी तरह का।


3
पुनरावृत्ति। मेमॉशन
जेम्स

2
जाहिर है, मेमोइज़ेशन केवल तब काम करता है जब वास्तव में दोहराया गणना हो।
जोर्ग डब्ल्यू मित्तग

2
यह भी ध्यान देने योग्य है कि सभी भाषा कार्यान्वयन वैसे भी पुनरावृत्ति अनुकूलन नहीं कर सकते
jk।

2
यह शायद पुनरावृत्ति की तुलना में corecursion के साथ बेहतर हल होगा।
जोर्ग डब्ल्यू मित्तग

3
यदि आप 1,000,000 से कम संख्या में काम कर रहे हैं और 1 पर जा रहे हैं, तो इस प्रश्न के उत्तर में पहुंचने के लिए लगभग 500 चरण शामिल हैं। यह एक छोटे स्टैक फ्रेम को देखते हुए कर पुनरावृत्ति नहीं होनी चाहिए। --- यदि आप 1 से शुरू करने के लिए हल करने का प्रयास कर रहे हैं, तो 2, 4, 8, 16, {5,32} का पालन करें और वहां से ऊपर जाएं, आप इसे गलत कर रहे हैं।

जवाबों:


35

टेल कॉल ऑप्टिमाइज़ेशन कई भाषाओं और कंपाइलरों में मौजूद है। इस स्थिति में, संकलक प्रपत्र के एक फ़ंक्शन को पहचानता है:

int foo(n) {
  ...
  return bar(n);
}

यहां, भाषा यह पहचानने में सक्षम है कि जो परिणाम लौटाया जा रहा है, वह किसी अन्य फ़ंक्शन का परिणाम है और एक जंप में नए स्टैक फ्रेम के साथ फ़ंक्शन कॉल को बदलें।

एहसास है कि क्लासिक factorial विधि:

int factorial(n) {
  if(n == 0) return 1;
  if(n == 1) return 1;
  return n * factorial(n - 1);
}

है निरीक्षण वापसी पर आवश्यक की वजह से पूंछ कॉल optimizatable। ( उदाहरण स्रोत कोड और संकलित आउटपुट )

इस टेल कॉल को अनुकूलन योग्य बनाने के लिए,

int _fact(int n, int acc) {
    if(n == 1) return acc;
    return _fact(n - 1, acc * n);
}

int factorial(int n) {
    if(n == 0) return 1;
    return _fact(n, 1);
}

इस कोड को gcc -O2 -S fact.c(-ओ 2 को कंपाइलर में ऑप्टिमाइज़ेशन सक्षम करने के लिए आवश्यक है, लेकिन -O3 के अधिक अनुकूलन के साथ इसे पढ़ना मानव के लिए कठिन हो जाता है ...)

_fact(int, int):
    cmpl    $1, %edi
    movl    %esi, %eax
    je  .L2
.L3:
    imull   %edi, %eax
    subl    $1, %edi
    cmpl    $1, %edi
    jne .L3
.L2:
    rep ret

( उदाहरण स्रोत कोड और संकलित आउटपुट )

एक सेगमेंट में देख सकता है .L3, jneबजाय एक call(जो एक नए स्टैक फ्रेम के साथ सबरूटीन कॉल करता है)।

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

ने कहा कि,

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


"पुनरावर्ती एल्गोरिथ्म में स्टैक ओवरफ्लो से बचने के तरीके" के सामान्य प्रश्न के लिए ...

एक और दृष्टिकोण एक पुनरावृत्ति काउंटर को शामिल करना है। यह किसी के नियंत्रण (और खराब कोडिंग) से परे स्थितियों के कारण होने वाली अनंत छोरों का पता लगाने के लिए अधिक है।

रिकर्सन काउंटर का रूप लेता है

int foo(arg, counter) {
  if(counter > RECURSION_MAX) { return -1; }
  ...
  return foo(arg, counter + 1);
}

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

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


सही एल्गोरिथ्म का उपयोग करें और सही समस्या को हल करें। Collatz अनुमान के लिए विशेष रूप से, ऐसा प्रतीत होता है कि आप इसे xkcd तरीके से हल करने का प्रयास कर रहे हैं :

XKCD # 710

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

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


1
मैं वेब पर सर्फिंग करते हुए आज उस xkcd कार्टून में आ गया था। :-) रान्डेल मुनरो के कार्टून एक खुशी की बात है।
लर्नकुरेव

@Lernkurve मैंने यह लिखने के बाद कोड एडिट करने पर गौर किया (और पोस्ट किया)। क्या आपको इसके लिए अन्य कोड नमूनों की आवश्यकता है?

नहीं, बिलकुल नहीं। यह एकदम सही है। पूछने के लिए एक गुच्छा धन्यवाद!
लर्नकुरेव

क्या मैं इस कार्टून को भी जोड़ने का सुझाव दे सकता हूं: imgs.xkcd.com/comics/functional.png
एलेन स्पार्टस

@espertus शुक्रिया। मैंने इसे जोड़ा है (कुछ स्रोत पीढ़ी को साफ किया और थोड़ा और जोड़ा)

17

ध्यान रखें कि भाषा कार्यान्वयन को पूंछ पुनरावृत्ति अनुकूलन का समर्थन करना चाहिए। मुझे नहीं लगता कि प्रमुख जावा कंपाइलर करते हैं।

संस्मरण का अर्थ है कि आपको हर बार इसे पुन: गणना करने के बजाय गणना का परिणाम याद है, जैसे:

collatz(i):
    if i in memoized:
        return memoized[i]

    if i == 1:
        memoized[i] = 1
    else if odd(i):
        memoized[i] = 1 + collatz(3*i + 1)
    else
        memoized[i] = 1 + collatz(i / 2)

    return memoized[i]

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


1
संस्मरण की बहुत समझदार व्याख्या। इन सबसे ऊपर, एक कोड स्निपेट के साथ इसे दर्शाने के लिए धन्यवाद। इसके अलावा, "दृश्यों के अंत में बहुत दोहराव होने वाला है" मेरे लिए चीजें स्पष्ट कर दीं। धन्यवाद।
लर्नकुरेव

10

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

यह StackOverflow प्रश्न जावा में ट्रम्पोलिनिंग के विभिन्न कार्यान्वयनों के बारे में बहुत अधिक विस्तार से बताता है: Trampoline के लिए जावा में StackOverflow को संभालना


मैंने इस समय के बारे में भी सोचा। Trampolines टेल कॉल ऑप्टिमाइज़ेशन करने की एक विधि है, इसलिए लोग इसे कहते हैं (sorta-लगभग-शायद)। विशिष्ट संदर्भ के लिए +1।
स्टीवन एवर्स

6

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

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

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


1
संस्मरण दृष्टिकोण को इंगित करने के लिए +1 भी उपयोगी है।
कार्ल बेज़ेलफेल्ट

सभी कार्यात्मक प्रोग्रामिंग भाषाओं में टेल कॉल ऑप्टिमाइज़ेशन है।

3

आप एक एन्यूमरेशन बना सकते हैं जो रिकर्सन की जगह लेगा ... यहाँ ऐसा करने वाले संकाय की गणना के लिए एक उदाहरण है ... (बड़ी संख्या के लिए काम नहीं करेगा क्योंकि मैं केवल उदाहरण में लंबे समय तक इस्तेमाल किया था :-))

public class Faculty
{

    public static IEnumerable<long> Faculties(long n)
    {
        long stopat = n;

        long x = 1;
        long result = 1;

        while (x <= n)
        {
            result = result * x;
            yield return result;
            x++;
        }
    }
}

भले ही यह संस्मरण नहीं है, इस तरह आप एक ढेर अतिप्रवाह को शून्य कर देंगे


संपादित करें


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

निम्नलिखित कोड

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

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

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Numerics;
using System.Text;
using System.Threading.Tasks;
using System.Timers;

namespace ConsoleApplication1
{
    class Program
    {
        static Stopwatch w = new Stopwatch();
        static Faculty f = Faculty.GetInstance();

        static void Main(string[] args)
        {
            Out(5);
            Out(10);
            Out(-5);
            Out(0);
            Out(1);
            Out(4);
            Out(29);
            Out(30);
            Out(20);
            Out(10000);
            Out(20000);
            Out(19999);
            Console.ReadKey();
        }

        static void Out(BigInteger n)
        {
             try
            {
                w.Reset();
                w.Start();
                var x = f.Calculate(n);
                w.Stop();
                var time = w.ElapsedMilliseconds;
                Console.WriteLine(String.Format("{0} ({2}ms): {1}", n, x, time));
            }
            catch (ArgumentException e)
            {
                Console.WriteLine(e.Message);
            }

            Console.WriteLine("\n\n");
       }
    }

मैं फैकल्टी क्लास में * 1 स्टैटिक वैरिएबल "इंस्टेंस" को एक सिंगलटन स्टोर के रूप में घोषित करता हूं। जब तक आपका कार्यक्रम चल रहा होता है, तब तक जब भी आप उस वर्ग के "GetInstance ()" को प्राप्त करते हैं, जिसने पहले से गणना किए गए सभी मान संग्रहीत किए हैं। * 1 स्टैटिक सॉर्टेलडिस्ट जो पहले से गणना किए गए सभी मूल्यों को रखेगा

कंस्ट्रक्टर में मैं इनपुट 1 के लिए सूची 1 के 2 विशेष मानों को भी जोड़ता हूं।

    public class Faculty
    {
        private static SortedList<BigInteger, BigInteger> _values; 
        private static Faculty _faculty {get; set;}

        private Faculty ()
        {
            _values = new SortedList<BigInteger, BigInteger>();
            _values.Add(0, 1);
            _values.Add(1, 1);
        }

        public static Faculty GetInstance() {
            _faculty = _faculty ?? new Faculty();
            return _faculty;
        }

        public BigInteger Calculate(BigInteger n) 
        {
            // check if input is smaller 0
            if (n < 0)
                throw new ArgumentException(" !!! Faculty is not defined for values < 0 !!!");

            // if value is not already calculated => do so
            if(!_values.ContainsKey(n))
                Faculties(n);

            // retrieve n! from Sorted List
            return _values[n];
        }

        private static void Faculties(BigInteger n)
        {
            // get the last calculated values and continue calculating if the calculation for a bigger n is required
            BigInteger i = _values.Max(x => x.Key),
                           result = _values[i];

            while (++i <= n)
            {
                CalculateNext(ref result, i);
                // add value to the SortedList if not already done
                if (!_values.ContainsKey(i))
                    _values.Add(i, result);
            }
        }

        private static void CalculateNext(ref BigInteger lastresult, BigInteger i) {

            // put in whatever iterative calculation step you want to do
            lastresult = lastresult * i;

        }
    }
}

5
तकनीकी रूप से यह पुनरावृत्ति है जैसा कि आपने पूरी तरह से किसी भी पुनरावृत्ति को हटा दिया
शाफ़्ट सनकी

यह :-) है और यह प्रत्येक गणना चरण के बीच विधियों चर के भीतर परिणामों को याद करता है
Ingo

2
मुझे लगता है कि आप गलतफहमी को याद करते हैं, जो तब होता है जब संकायों (100) को पहली बार कहा जाता है कि यह परिणाम की गणना करता है और इसे एक हैश में संग्रहीत करता है और वापस लौटता है, फिर जब इसे फिर से संग्रहीत परिणाम कहा जाता है
शाफ़्ट सनकी

@jk। अपने क्रेडिट के लिए, वह वास्तव में कभी नहीं कहते हैं कि यह पुनरावर्ती है।
नील

भले ही इस Memoization नहीं है, इस तरह से आप एक ढेर अतिप्रवाह शून्य होगा
इंगो

2

स्काला के लिए, आप @tailrecएनोटेशन को पुनरावर्ती विधि में जोड़ सकते हैं । इस तरह संकलक सुनिश्चित करता है कि पूंछ कॉल अनुकूलन वास्तव में हुआ:

तो यह संकलन नहीं होगा (भाज्य):

@tailrec
def fak1(n: Int): Int = {
  n match {
    case 0 => 1
    case _ => n * fak1(n - 1)
  }
}

त्रुटि संदेश है:

scala: @atelrec एनोटेट विधि fak1 का अनुकूलन नहीं कर सका: इसमें पुनरावर्ती कॉल नहीं है जो पूंछ की स्थिति में नहीं है

दूसरी ओर:

def fak3(n: Int): Int = {
  @tailrec
  def fak3(n: Int, result: Int): Int = {
    n match {
      case 0 => result
      case _ => fak3(n - 1, n * result)
    }
  }

  fak3(n, 1)
}

संकलन, और पूंछ कॉल अनुकूलन हुआ।


1

एक संभावना जिसका उल्लेख अभी तक नहीं किया गया है वह है पुनरावृत्ति, लेकिन सिस्टम स्टैक का उपयोग किए बिना। बेशक आप अपने ढेर को भी उखाड़ फेंक सकते हैं, लेकिन अगर आपके एल्गोरिथ्म को वास्तव में एक फॉर्म या किसी अन्य में बैकट्रैकिंग की आवश्यकता है (क्यों अन्यथा पुनरावृत्ति का उपयोग करना?), तो आपके पास कोई विकल्प नहीं है।

कुछ भाषाओं के स्टैकलेस कार्यान्वयन हैं, जैसे स्टैकलेस पायथन


0

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

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