Kuyruk özyineleme nedir?


52

Genel özyineleme kavramını biliyorum. Bataklık algoritması çalışırken kuyruk özyinelemesi kavramına rastladım . Bu hızlı sıralama algoritmasının MIT'den 18: 30 saniyede videoda profesör, bunun bir özyinelemeli özyinelemeli algoritma olduğunu söylüyor. Bana kuyruk özyinelemenin gerçekte ne anlama geldiği açık değil.

Birisi kavramı uygun bir örnekle açıklayabilir mi?

Burada SO topluluğu tarafından verilen bazı cevaplar .


Kuyruk özyineleme terimiyle karşılaştığınız bağlam hakkında daha fazla bilgi verin . Bağlansın mı? Alıntı?
A.Schulz

@ A.Schulz Bağlamı içeriğe bağladım.
Geek

5
"Bak kuyruk özyineleme nedir? " Stackoverflow
Vor

2
@ ajmartin Soru Yığın Taşması sınırındadır ancak Bilgisayar Bilimi üzerine kesin olarak açıktır , bu nedenle prensipte Bilgisayar Bilimi daha iyi cevaplar vermelidir. Burada olmadı, ama daha iyi bir cevap umuduyla burada tekrar sormak hala sorun değil. Geek, SO hakkındaki önceki sorunuzdan bahsetmeliydiniz, böylece insanlar zaten söylenenleri tekrar etmiyorlardı.
Gilles 'SO- kötülük' dur '

1
Ayrıca, belirsiz olanın ne olduğunu veya neden önceki cevaplardan memnun olmadığınızı söylemelisiniz, bence SO insanlar iyi cevaplar veriyorlar ama tekrar sormanıza ne sebep oldu?

Yanıtlar:


52

Kuyruk özyineleme, özyinelemeli bir çağrı yaptıktan sonra çağrı işlevinin artık hesaplama yapmadığı özel bir özyineleme durumudur. Örneğin, işlev

int f (int x, int y) {
  eğer (y == 0) {
    x döndür;
  }

  dönüş f (x * y, y-1);
}

kuyruk özyinelemelidir (son komut özyinelemeli bir çağrı olduğundan), bu işlev kuyruk özyinelemeli değildir:

int g (int x) {
  eğer (x == 1) {
    dönüş 1;
  }

  int y = g (x-1);

  x * y dönüşü;
}

özyinelemeli çağrı döndükten sonra bazı hesaplama yaptığı için.

Kuyruk özyineleme önemlidir, çünkü genel özyinelemeden daha verimli bir şekilde uygulanabilir. Normal bir özyinelemeli çağrı yaptığımızda, geri dönüş adresini çağrı yığınının üstüne itip sonra çağrılan işleve atlamamız gerekir. Bu, özyinelemeli aramaların derinliğinde doğrusal olan bir arama yığına ihtiyacımız olduğu anlamına gelir. Kuyruk özyinelememiz olduğunda özyinelemeli çağrıdan geri döndüğümüzde, hemen geri döneceğimizi biliyoruz, böylece geri dönen işlevler zincirinin tamamını atlayarak doğrudan orijinal arayana geri dönebiliriz. Bu, özyinelemeli aramaların tümü için bir arama yığınına hiç ihtiyacımız olmadığı ve son aramayı basit bir atlama olarak uygulayabildiğimiz anlamına gelir, bu da bize yer kazandırır.


2
"Bu özyinelemeli aramalar için hiç bir çağrı yığınına ihtiyacımız yok demektir" yazdınız. Çağrı yığını her zaman orada olacak, sadece dönüş adresinin çağrı yığınına yazılmasına gerek yok, değil mi?
Geek

2
Bir dereceye kadar hesaplama modelinize bağlı :) Ama evet, gerçek bir bilgisayarda çağrı yığını hala orada, biz onu kullanmıyoruz.
Matt Lewis

Ya son çağrıysa, ama döngü için. Öyleyse yukarıdaki tüm hesaplamaları yapıyorsunuz, ancak bazıları gibi bir döngüdedef recurse(x): if x < 0 return 1; for i in range 100{ (do calculations) recurse(x)}
thed0ctor

13

Basitçe söylemek gerekirse, kuyruk özyinelemesi, derleyicinin özyinelemeli çağrıyı "goto" komutuyla değiştirebileceği bir özyinelemedir, bu nedenle derlenen sürümün yığın derinliğini arttırması gerekmez.

Bazen kuyruk özyinelemeli bir işlev tasarlamak, ek parametreler içeren bir yardımcı işlev oluşturmanız gerekir.

Örneğin, bu değil bir kuyruk özyinelemeli fonksiyon:

int factorial(int x) {
    if (x > 0) {
        return x * factorial(x - 1);
    }
    return 1;
}

Ancak bu kuyruk özyinelemeli bir işlevdir:

int factorial(int x) {
    return tailfactorial(x, 1);
}

int tailfactorial(int x, int multiplier) {
    if (x > 0) {
        return tailfactorial(x - 1, x * multiplier);
    }
    return multiplier;
}

çünkü derleyici özyinelemeli işlevi özyinelemeli olmayan bir işlevle yeniden yazabilirdi (şuna benzer):

int tailfactorial(int x, int multiplier) {
    start:
    if (x > 0) {
        multiplier = x * multiplier;
        x--;
        goto start;
    }
    return multiplier;
}

Derleyici kuralı çok basittir: " return thisfunction(newparameters);" bulduğunuzda " " ile değiştirin parameters = newparameters; goto start;. Ancak bu, özyinelemeli çağrının döndürdüğü değer doğrudan döndürüldüğünde yapılabilir.

Eğer bir fonksiyondaki bütün özyinelemeli çağrılar böyle değiştirilebilirse, o zaman bir özyinelemeli fonksiyondur.


13

Cevabım, Bilgisayar Programlarının Yapısı ve Yorumlanması kitabında verilen açıklamaya dayanmaktadır . Bu kitabı bilgisayar bilimcilerine şiddetle tavsiye ediyorum.

Yaklaşım A: Doğrusal Özyinelemeli Süreç

(define (factorial n)
 (if (= n 1)
  1
  (* n (factorial (- n 1)))))

Yaklaşım A için sürecin şekli şuna benzer:

(factorial 5)
(* 5 (factorial 4))
(* 5 (* 4 (factorial 3)))
(* 5 (* 4 (* 3 (factorial 2))))
(* 5 (* 4 (* 3 (* 2 (factorial 1)))))
(* 5 (* 4 (* 3 (* 2 (* 1)))))
(* 5 (* 4 (* 3 (* 2))))
(* 5 (* 4 (* 6)))
(* 5 (* 24))
120

Yaklaşım B: Doğrusal İteratif Süreç

(define (factorial n)
 (fact-iter 1 1 n))

(define (fact-iter product counter max-count)
 (if (> counter max-count)
  product
  (fact-iter (* counter product)
             (+ counter 1)
             max-count)))

Yaklaşım B için sürecin şekli şuna benzer:

(factorial 5)
(fact-iter 1 1 5)
(fact-iter 1 2 5)
(fact-iter 2 3 5)
(fact-iter 6 4 5)
(fact-iter 24 5 5)
(fact-iter 120 6 5)
120

Doğrusal Yinelemeli İşlem (Yaklaşım B), işlem özyinelemeli bir prosedür olsa bile sabit alanda çalışır. Ayrıca, bu yaklaşımda, bir değişkenlerin işlemin durumunu herhangi bir noktada tanımladıkları da belirtilmelidir. {product, counter, max-count}. Bu aynı zamanda kuyruk özyinelemesinin derleyici optimizasyonuna izin verdiği bir tekniktir.

Yaklaşım A'da, tercümanın hangi ertelenmiş işlemlerin zinciri olduğunu temelde tuttuğu daha gizli bilgiler vardır.


5

Kuyruk özyineleme özyinelemeli çağrıların işlevdeki son komutlar olduğu (özyükleme kısmının geldiği yer) özyinelemenin bir şeklidir. Dahası, özyinelemeli çağrı, önceki değerleri depolayan bellek işlevlerine referanslarla (işlev parametrelerinin dışındaki referanslar) oluşmamalıdır. Bu şekilde, önceki değerleri önemsemeyiz ve özyinelemeli aramaların tümü için bir yığın çerçevesi yeterlidir; kuyruk özyineleme özyinelemeli algoritmaları optimize etmenin bir yoludur. Diğer bir avantaj / optimizasyon, kuyruk özyinelemeli algoritmayı özyineleme yerine yinelemeyi kullanan eşdeğerine dönüştürmenin kolay bir yoludur. Yani evet, quicksort için algoritma gerçekten kuyruk özyinelemeli.

QUICKSORT(A, p, r)
    if(p < r)
    then
        q = PARTITION(A, p, r)
        QUICKSORT(A, p, q–1)
        QUICKSORT(A, q+1, r)

İşte yinelemeli sürüm:

QUICKSORT(A)
    p = 0, r = len(A) - 1
    while(p < r)
        q = PARTITION(A, p, r)
        r = q - 1

    p = 0, r = len(A) - 1
    while(p < r)
        q = PARTITION(A, p, r)
        p = q + 1
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.