Apa itu rekursi ekor?

52

Saya tahu konsep umum rekursi. Saya menemukan konsep rekursi ekor sambil mempelajari algoritma quicksort. Dalam video algoritma pengurutan cepat ini dari MIT pada pukul 18:30, profesor mengatakan bahwa ini adalah algoritme rekursif ekor. Tidak jelas bagi saya apa yang dimaksud dengan rekursi ekor sebenarnya.

Adakah yang bisa menjelaskan konsep tersebut dengan contoh yang tepat?

Beberapa jawaban diberikan oleh komunitas SO di sini .

Kutu buku
sumber
Ceritakan lebih banyak tentang konteks di mana Anda mengalami istilah rekursi ekor . Tautan? Kutipan?
A.Schulz
@ A.Schulz Saya telah meletakkan tautan ke konteksnya.
Geek
5
Lihatlah " Apa itu rekursi ekor? " Di stackoverflow
Vor
2
@ajmartin Pertanyaannya adalah batas pada Stack Overflow tetapi tegas pada topik tentang Ilmu Komputer , jadi pada prinsipnya Ilmu Komputer harus menghasilkan jawaban yang lebih baik. Itu belum terjadi di sini, tetapi masih boleh bertanya kembali di sini dengan harapan jawaban yang lebih baik. Geek, Anda seharusnya sudah menyebutkan pertanyaan Anda sebelumnya pada SO, sehingga orang tidak mengulangi apa yang sudah dikatakan.
Gilles 'SO- stop being evil'
1
Anda juga harus mengatakan apa yang ambigu atau mengapa Anda tidak puas dengan jawaban sebelumnya, saya pikir pada SO orang memberikan jawaban yang baik tetapi apa yang menyebabkan Anda bertanya lagi?

Jawaban:

52

Rekursi ekor adalah kasus rekursi khusus di mana fungsi panggilan tidak lagi menghitung setelah melakukan panggilan rekursif. Misalnya saja fungsinya

int f (int x, int y) {
  if (y == 0) {
    return x;
  }

  return f (x * y, y-1);
}

adalah ekor rekursif (karena instruksi terakhir adalah panggilan rekursif) sedangkan fungsi ini bukan ekor rekursif:

int g (int x) {
  if (x == 1) {
    return 1;
  }

  int y = g (x-1);

  return x * y;
}

karena melakukan beberapa perhitungan setelah panggilan rekursif telah kembali.

Rekursi ekor penting karena dapat diimplementasikan lebih efisien daripada rekursi umum. Ketika kita melakukan panggilan rekursif normal, kita harus mendorong alamat pengirim ke tumpukan panggilan kemudian melompat ke fungsi yang dipanggil. Ini berarti bahwa kita memerlukan tumpukan panggilan yang ukurannya linier di kedalaman panggilan rekursif. Ketika kita memiliki rekursi ekor, kita tahu bahwa segera setelah kita kembali dari panggilan rekursif kita juga akan segera kembali, sehingga kita dapat melewatkan seluruh rantai fungsi rekursif yang kembali dan kembali langsung ke pemanggil yang asli. Itu berarti kita tidak memerlukan tumpukan panggilan sama sekali untuk semua panggilan rekursif, dan dapat mengimplementasikan panggilan terakhir sebagai lompatan sederhana, yang menghemat ruang bagi kita.

Matt Lewis
sumber
2
Anda menulis "Itu berarti kita tidak perlu tumpukan panggilan sama sekali untuk semua panggilan rekursif". Tumpukan panggilan akan selalu ada, hanya saja alamat pengirim tidak perlu ditulis ke dalam tumpukan panggilan, bukan?
Geek
2
Itu tergantung pada model perhitungan Anda sampai tingkat tertentu :) Tapi ya, di komputer nyata tumpukan panggilan masih ada, kami hanya tidak menggunakannya.
Matt Lewis
Bagaimana jika itu panggilan terakhir tetapi untuk for loop. Jadi Anda melakukan semua perhitungan Anda di atas tetapi beberapa di antaranya untuk loop sepertidef recurse(x): if x < 0 return 1; for i in range 100{ (do calculations) recurse(x)}
thed0ctor
13

Secara sederhana, rekursi ekor adalah rekursi di mana kompiler dapat mengganti panggilan rekursif dengan perintah "goto", sehingga versi yang dikompilasi tidak harus meningkatkan kedalaman tumpukan.

Kadang-kadang merancang fungsi rekursif ekor mengharuskan Anda perlu membuat fungsi pembantu dengan parameter tambahan.

Sebagai contoh, ini bukan fungsi rekursif ekor:

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

Tapi ini adalah fungsi rekursif ekor:

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

karena kompiler dapat menulis ulang fungsi rekursif ke yang non-rekursif, menggunakan sesuatu seperti ini (kodesemu):

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

Aturan untuk kompiler sangat sederhana: Ketika Anda menemukan " return thisfunction(newparameters);", ganti dengan " parameters = newparameters; goto start;". Tetapi ini hanya dapat dilakukan jika nilai yang dikembalikan oleh panggilan rekursif dikembalikan secara langsung.

Jika semua panggilan rekursif dalam suatu fungsi dapat diganti seperti ini, maka itu adalah fungsi ekor-rekursif.

Viliam Búr
sumber
13

Jawaban saya didasarkan pada penjelasan yang diberikan dalam buku Struktur dan Interpretasi Program Komputer . Saya sangat merekomendasikan buku ini kepada para ilmuwan komputer.

Pendekatan A: Proses Rekursif Linear

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

Bentuk proses untuk Pendekatan A terlihat seperti ini:

(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

Pendekatan B: Proses Iteratif Linier

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

Bentuk proses untuk Pendekatan B terlihat seperti ini:

(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

Proses Iteratif Linear (Pendekatan B) berjalan dalam ruang konstan walaupun prosesnya adalah prosedur rekursif. Juga harus dicatat bahwa dalam pendekatan ini satu set variabel menentukan keadaan proses pada setiap titik yaitu. {product, counter, max-count}. Ini juga merupakan teknik di mana rekursi ekor memungkinkan optimisasi kompiler.

Dalam Pendekatan A, terdapat lebih banyak informasi tersembunyi yang disimpan oleh juru bahasa yang pada dasarnya merupakan rantai operasi yang ditangguhkan.

ajmartin
sumber
5

Ekor-rekursi adalah bentuk rekursi di mana panggilan rekursif adalah instruksi terakhir dalam fungsi (dari situlah bagian ekor berasal). Selain itu, panggilan rekursif tidak boleh dibuat dengan referensi ke sel memori yang menyimpan nilai sebelumnya (referensi selain parameter fungsi). Dengan cara ini, kami tidak peduli dengan nilai-nilai sebelumnya dan satu frame stack mencukupi untuk semua panggilan rekursif; ekor-rekursi adalah salah satu cara untuk mengoptimalkan algoritma rekursif. Keuntungan lain / optimasi adalah bahwa ada cara mudah untuk mengubah algoritma rekursif ekor menjadi yang setara yang menggunakan iterasi bukan rekursi. Jadi ya, algoritma untuk quicksort memang ekor-rekursif.

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

Ini adalah versi yang berulang:

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
saadtaame
sumber