Özyinelemeli bir algoritmada yığın taşması önlemek için hangi yöntemler vardır?


44

Soru

Özyinelemeli bir algoritmadan kaynaklanan yığın taşmasını çözmenin olası yolları nelerdir?

Örnek

Project Euler 14 problemini çözmeye çalışıyorum ve özyinelemeli bir algoritmayla denemeye karar verdim. Ancak, program bir java.lang.StackOverflowError ile durur. Anlaşılır. Algoritma aslında yığının üzerinden geçti çünkü çok büyük bir sayı için bir Collatz dizisi oluşturmaya çalıştım.

Çözümler

Bu yüzden merak ediyordum: özyinelemeli algoritmanızın doğru yazıldığını ve yığının her zaman taşması ile sonuçlanacağını varsayarak bir yığın taşmasını çözmek için hangi standart yollar var? Akla gelen iki kavram:

  1. kuyruk özyineleme
  2. tekrarlama

Fikirler (1) ve (2) doğru mu? Başka seçenekler var mı?

Düzenle

Tercihen Java, C #, Groovy veya Scala'da bazı kodları görmenize yardımcı olur.

Belki yukarıda belirtilen Project Euler problemini kullanmayın, böylece başkaları için bozulmaz, fakat başka bir algoritma alır. Faktör belki veya benzer bir şey.


3
Yineleme. Anlaşma
James,

2
Aslında orada ne zaman Açıkçası, Memoization çalışır edilir hesaplama tekrarladı.
Jörg W Mittag

2
Ayrıca, tüm dil uygulamalarının yine de kuyruk özyineleme optimizasyonları yapamayacağına dikkat
çekmek gerekir

2
Bu muhtemelen özyinelemeyle özyinelemeden daha iyi çözülecektir.
Jörg W Mittag

3
Eğer 1.000.000'den daha az sayıda çalışıyorsanız ve 1'e gidecekseniz, bu sorunun cevabı 1'e ulaşmak için yaklaşık 500 adımdan oluşuyor. --- 1'den başlayarak çözmeyi deniyorsanız, ardından 2, 4, 8, 16, {5,32} 'a kadar takip edip oradan yukarı çıkın, yanlış yapıyorsunuz demektir.

Yanıtlar:


35

Kuyruk çağrısı optimizasyonu birçok dilde ve derleyicide mevcuttur. Bu durumda, derleyici formun bir fonksiyonunu tanır:

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

Burada dil, döndürülen sonucun başka bir fonksiyonun sonucudur ve yeni bir yığın çerçeveli bir fonksiyon çağrısını bir atlamaya dönüştürür.

Klasik faktöriyel yöntemin farkına varın:

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

olduğu değil çünkü dönüş gerekli denetim kuyruk çağrı optimizatable. ( Örnek kaynak kodu ve derlenmiş çıktı )

Bu kuyruk aramasını optimize edilebilir hale getirmek için,

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);
}

Bu kodu gcc -O2 -S fact.cderlemek (derleyicideki optimizasyonu sağlamak için -O2 gereklidir, fakat -O3'ün daha fazla optimizasyonu ile bir insanın okuması zorlaşır ...)

_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

( Örnek kaynak kodu ve derlenmiş çıktı )

Bir segment görebilir .L3, jneyerine göre call(yeni bir yığın çerçeveye sahip olan bir alt yordam çağrı yapar).

Lütfen bunun C ile yapıldığına dikkat edin. Java'da kuyruk çağrısı optimizasyonu zordur ve JVM uygulamasına bağlıdır (ki, bunu yapmak için hiçbir şey görmedim, çünkü zor ve yığın çerçeveleri gerektiren gerekli Java güvenlik modelinin etkileri) - bu, TCO'nun önlediği şeydir) - tail-recursion + java ve tail-recursion + optimizasyonu , göz atmak için iyi etiket kümeleridir. Diğer JVM dillerinin kuyruk özyinelemesini daha iyi optimize edebildiğini görebilirsiniz (clojure'u deneyin ( arama sırasını optimize etmek için tekrarlama gerektirir ) veya ölçeklendirme).

Bahsedilen,

Bir şeyi doğru yazdığınızı bilmenin kesin bir neşesi var - yapılabilecek en ideal şekilde.
Şimdi biraz viski alacağım ve biraz Alman elektroniği yapacağım ...


"Özyinelemeli bir algoritmada yığın taşması önleme yöntemleri" genel sorusuna ...

Diğer bir yaklaşım ise özyinelemeli bir sayaç içermektir. Bu, kontrolünün dışındaki (ve zayıf kodlama) durumların neden olduğu sonsuz döngüleri tespit etmek için daha fazladır.

Özyineleme sayacı şeklini alır.

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

Her arama yaptığınızda, sayacı artırırsınız. Sayaç çok büyürse, hata yaparsınız (burada, sadece -1 dönüş, ancak diğer dillerde istisna atmayı tercih edebilirsiniz). Fikir, beklenenden çok daha derin ve muhtemelen sonsuz bir döngü olan bir özyineleme yaparken daha kötü şeylerin oluşmasını (bellek dışı hatalar) önlemektir.

Teoride buna ihtiyacın olmamalı. Uygulamada, küçük hatalardan ve kötü kodlama uygulamalarından oluşan bir bolluk yüzünden buna isabet eden kötü yazılmış bir kod gördüm (çok iş parçacıklı eşzamanlılık sorunları, başka bir iş parçacığını sonsuz özyinelemeli çağrılara götüren yöntemin dışında bir şey değiştiren sorunlar).


Doğru algoritmayı kullanın ve doğru problemi çözün. Özellikle Collatz Sanısı için, göründüğü sen bunu çözmeye çalıştıklarını xkcd şekilde:

XKCD # 710

Bir numaradan başlıyorsunuz ve bir ağaç geçişi yapıyorsunuz. Bu hızla çok geniş bir arama alanına yol açar. Doğru cevap için yineleme sayısını hesaplamak için yapılan hızlı bir işlem yaklaşık 500 adımda sonuçlanır. Bu küçük bir yığın çerçeveli özyinelemeyle ilgili bir sorun olmamalıdır.

Özyinelemeli çözümü bilmek kötü bir şey olmasa da, yinelemeli çözümün daha iyi olduğunu defalarca fark etmek gerekir . Özyinelemeli bir algoritmayı yinelemeli bir algoritmaya dönüştürmeye yaklaşmanın birkaç yolu, özyinelemeden yinelemeye geçmek için Yol Taşma Taşması'nda görülebilir .


1
Bugün internette gezinirken o xkcd çizgi filmle karşılaştım. :-) Randall Munroe'nın karikatürleri bir zevktir.
Lernkurve

@ Lernkurve Bunu yazmaya başladığımda (ve gönderdikten sonra) kod düzenlemesinin eklendiğini fark ettim. Bunun için başka kod örneklerine ihtiyacınız var mı?

Hayır, hiç de değil. Mükemmel. Sorduğun için teşekkürler!
Lernkurve

Bu karikatürü eklemeyi önerebilir miyim: imgs.xkcd.com/comics/functional.png
Ellen Spertus

@espertus teşekkür ederim. Ben ekledim (bazı kaynak nesilleri temizledik ve biraz daha ekledik)

17

Dil uygulamasının kuyruk özyineleme optimizasyonunu desteklemesi gerektiğini unutmayın. Büyük java derleyicilerinin yaptığını sanmıyorum.

Notlandırma, her seferinde yeniden hesaplamak yerine bir hesaplamanın sonucunu hatırladığınız anlamına gelir, örneğin:

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]

Her diziyi bir milyondan daha az hesaplarken, dizilerin sonunda çok fazla tekrarlama olacak. Notlandırma, yığını daha derin ve daha derin yapmak zorunda kalmak yerine, önceki değerler için hızlı bir karma tablo araması yapar.


1
Dekontun çok anlaşılır bir açıklaması. Her şeyden önce, bir kod pasajı ile gösterdiğiniz için teşekkür ederiz. Ayrıca, "dizilerin sonunda çok fazla tekrar olacak" benim için net bir şey yaptı. Teşekkür ederim.
Lernkurve

10

Henüz kimsenin tramplenden bahsetmediğinden şaşırdım . Bir trambolin (bu anlamda) yinelemeli olarak thunk-returning fonksiyonlarını çağıran (sürekli geçiş tarzı) bir döngüdür ve istifleme yönelimli fonksiyon çağrıları yığın yönelimli bir programlama dilinde uygulamak için kullanılabilir.

Bu StackOverflow sorusu, Java'daki çeşitli trambolin uygulamaları hakkında biraz daha ayrıntıya giriyor: StackOverflow'u Java'da Trampoline için Kullanma


Bunu da hemen düşündüm. Trambolinler kuyruk çağrısı optimizasyonu gerçekleştirmek için bir yöntemdir, bu yüzden insanlar (belki neredeyse sorta) söylüyorlar. +1 Özel referans için.
Steven Evers,

6

Kuyruk özyinelemeli işlevleri tanıyan ve bunları düzgün bir şekilde işleyen bir dil ve derleyici kullanıyorsanız (yani, “arayanı yerine kılavuzla değiştirir”), o zaman evet, küme kontrolden çıkmamalıdır. Bu optimizasyon özyinelemeli yöntemi yinelemeli olana indirgemektedir. Java'nın bunu yaptığını sanmıyorum, ama raketin yaptığını biliyorum.

Özyinelemeli bir yaklaşım yerine yinelemeli bir yaklaşımla giderseniz, aramaların nereden geldiğini hatırlama ihtiyacını ortadan kaldırıyorsunuz ve yığın taşması ihtimalini ortadan kaldırıyorsunuz (yinelemeli aramalardan).

Notlandırma harikadır ve genel hesaplamanızın birçok küçük, tekrarlanan hesaplamaya tabi olacağı göz önüne alındığında, bir önbellekte önceden hesaplanmış sonuçları arayarak toplam yöntem çağrısı sayısını azaltabilir. Bu fikir harika - yinelemeli bir yaklaşım mı yoksa özyinelemeli bir yaklaşım mı kullandığınızdan bağımsız.


1
Not almayı işaretlemek için +1, yinelemeli yaklaşımlarda da faydalıdır.
Karl Bielefeldt

Tüm fonksiyonel programlama dilleri kuyruk çağrısı optimizasyonuna sahiptir.

3

özyinelemenin yerini alacak bir Numaralandırma oluşturabilirsiniz ... işte bunu yapan fakülteyi hesaplamak için bir örnek ... (sadece örnekte uzun süre kullandığım gibi büyük sayılar için işe yaramaz)

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++;
        }
    }
}

bu bir not almamış olsa bile, bu şekilde bir yığın taşmasını geçersiz kılarsınız


DÜZENLE


Bazılarınızı üzdüysem özür dilerim. Tek niyetim yığın taşmasının önlenmesinin bir yolunu göstermekti. Muhtemelen hızlıca yazılmış ve kaba bir kod alıntıdan sadece küçük bir parça yerine tam kod örneği yazmalıydım.

Aşağıdaki kod

  • yinelemeli olarak gerekli değerleri hesaplarken kullandığımda özyinelemeyi önler.
  • önceden hesaplanmış değerler saklanır ve önceden hesaplanmışsa alınır
  • ayrıca bir kronometre içerir, böylece notların doğru şekilde çalıştığını görebilirsiniz.

... umm ... çalıştırırsanız, komut kabuğu pencerenizi 9999 satırlık bir arabellek olacak şekilde ayarladığınızdan emin olun ... normal 300, aşağıdaki programın sonuçlarına ulaşmaya yetmeyecektir ...

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");
       }
    }

Fakülte sınıfındaki * 1 statik değişkeni "örnek" olarak bir singleton mağazasına ilan ediyorum. Bu şekilde, programınız çalıştığı sürece, sınıfın "GetInstance ()" ne zaman hesaplarsanız, hesaplanan tüm değerleri saklayan örneği alırsınız. * 1 hesaplanan tüm değerleri tutacak statik SortedList

Yapıcıda ayrıca 0 ve 1 girişleri için listenin 2 özel değerini de eklerim.

    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
teknik olarak bu tamamen herhangi bir özyinelemeyi tamamen kaldırdığın yinelemedir
cırcır ucube

:-) olduğunu ve her hesaplama basamağı arasındaki metot değişkenleri içindeki sonuçları not eder
Ingo

2
Ben fakülte (100), bir karma sonucu ve depolar hesaplar ilk kez denilen ve iade edildiğinde tekrar saklanan sonucu döndürülür denir sonra zaman hangi sen yanlış anlamak memoisation düşünüyorum
mandal ucube

@jk. Kredisine göre, aslında bunun özyineli olduğunu söylemedi.
Neil

Bu bir faturalama olmasa bile, bu şekilde bir yığın taşmasını geçersiz kılacaksınız
Ingo

2

Scala'ya gelince, @tailrecek notu özyinelemeli bir yönteme ekleyebilirsiniz . Bu şekilde derleyici sağlayan bu kuyruk çağrı optimizasyonu aslında gerçekleşti:

Yani bu derlenmeyecek (faktöriyel):

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

hata mesajı:

scala: @tailrec açıklamalı yöntem fak1'i optimize edemedi: kuyruk konumunda olmayan özyinelemeli bir çağrı içeriyor

Diğer yandan:

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)
}

derler ve kuyruk çağrısı optimizasyonu yapıldı.


1

Henüz belirtilmemiş olan bir olasılık, özyinelemeye sahip olmak, ancak bir sistem yığını kullanmamaktır. Tabi ki öbeklerinizden de taşabilirsiniz, ancak algoritmanızın gerçekten bir biçimde veya başka bir şekilde geri izlemesi gerekiyorsa (neden özyinelemeyi kullanıyorsun?), Başka seçeneğiniz yok.

Bazı dillerin yığınsız uygulamaları vardır, örneğin Stackless Python .


0

Başka bir çözüm, kendi yığınızı taklit etmek ve derleyici + çalışma zamanının uygulanmasına bağlı kalmamaktır. Bu basit bir çözüm ya da hızlı bir çözüm değil, ancak teorik olarak StackOverflow'u yalnızca bellek yetersiz olduğunda alırsınız.

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.