Saya hampir mengerti cara kerja rekursi ekor dan perbedaan antara rekursi itu dan rekursi normal. Saya hanya tidak mengerti mengapa tidak memerlukan tumpukan untuk mengingat alamat pengirimnya.
// tail recursion
int fac_times (int n, int acc) {
if (n == 0) return acc;
else return fac_times(n - 1, acc * n);
}
int factorial (int n) {
return fac_times (n, 1);
}
// normal recursion
int factorial (int n) {
if (n == 0) return 1;
else return n * factorial(n - 1);
}
Tidak ada yang bisa dilakukan setelah memanggil fungsi itu sendiri dalam fungsi rekursi ekor tapi itu tidak masuk akal bagi saya.
c
algorithm
recursion
tail-recursion
Alan Coromano
sumber
sumber
-O3
. Tautan ini untuk diskusi sebelumnya yang mencakup dasar yang sangat mirip dan membahas apa yang diperlukan untuk menerapkan pengoptimalan ini.Jawaban:
Kompiler hanya dapat mengubah ini
menjadi seperti ini:
sumber
Anda bertanya mengapa "tidak memerlukan tumpukan untuk mengingat alamat pengirimnya".
Saya ingin membalikkan ini. Ini tidak menggunakan stack untuk mengingat alamat pengirim. Triknya adalah bahwa fungsi di mana rekursi tail terjadi memiliki alamat pengembaliannya sendiri di tumpukan, dan ketika ia melompat ke fungsi yang dipanggil, ia akan memperlakukan ini sebagai alamat pengembaliannya sendiri.
Secara konkret, tanpa pengoptimalan panggilan ekor:
Dalam hal ini, ketika
g
dipanggil, tumpukan akan terlihat seperti:Di sisi lain, dengan pengoptimalan panggilan ekor:
Dalam hal ini, ketika
g
dipanggil, tumpukan akan terlihat seperti:Jelas, ketika
g
kembali, itu akan kembali ke lokasi asalf
panggilan.EDIT : Contoh di atas menggunakan kasus di mana satu fungsi memanggil fungsi lain. Mekanismenya identik saat fungsi memanggil dirinya sendiri.
sumber
Rekursi tail biasanya dapat diubah menjadi loop oleh compiler, terutama jika akumulator digunakan.
akan dikompilasi menjadi sesuatu seperti
sumber
Ada dua elemen yang harus ada dalam fungsi rekursif:
Fungsi rekursif "biasa" menyimpan (2) di bingkai tumpukan.
Nilai yang dikembalikan dalam fungsi rekursif reguler terdiri dari dua jenis nilai:
Mari kita lihat contoh Anda:
Bingkai f (5) "menyimpan" hasil perhitungannya sendiri (5) dan nilai f (4), misalnya. Jika saya memanggil faktorial (5), tepat sebelum panggilan tumpukan mulai runtuh, saya memiliki:
Perhatikan bahwa setiap tumpukan menyimpan, selain nilai yang saya sebutkan, seluruh cakupan fungsi. Jadi, penggunaan memori untuk fungsi rekursif f adalah O (x), di mana x adalah jumlah panggilan rekursif yang harus saya lakukan. Jadi, jika saya membutuhkan RAM 1kb untuk menghitung faktorial (1) atau faktorial (2), saya perlu ~ 100k untuk menghitung faktorial (100), dan seterusnya.
Fungsi Tail Recursive menempatkan (2) di argumennya.
Dalam Rekursi Ekor, saya meneruskan hasil penghitungan parsial di setiap bingkai rekursif ke yang berikutnya menggunakan parameter. Mari kita lihat contoh faktorial kita, Rekursif Ekor:
int factorial (int n) {int helper (int num, int akumulasi) {if num == 0 return akumulasi else return helper (num - 1, akumulasi * num)} return helper (n, 1)
}
Mari kita lihat bingkainya secara faktorial (4):
Lihat perbedaannya? Dalam panggilan rekursif "biasa", fungsi kembalian secara rekursif menyusun nilai akhir. Dalam Tail Recursion, mereka hanya mereferensikan kasus dasar (yang terakhir dievaluasi) . Kami menyebut akumulator sebagai argumen yang melacak nilai yang lebih lama.
Template Rekursi
Fungsi rekursif reguler adalah sebagai berikut:
Untuk mengubahnya menjadi rekursi Tail, kami:
Lihat:
Lihat perbedaannya?
Pengoptimalan Tail Call
Karena tidak ada status yang disimpan di tumpukan Non-Border-Case dari Tail Call, mereka tidak begitu penting. Beberapa bahasa / interpreter kemudian mengganti tumpukan lama dengan yang baru. Jadi, tanpa frame stack yang membatasi jumlah panggilan, Tail Calls berperilaku seperti loop-ke dalam kasus ini.
Terserah kompiler Anda untuk mengoptimalkannya, atau tidak.
sumber
Berikut adalah contoh sederhana yang menunjukkan cara kerja fungsi rekursif:
Rekursi tail adalah fungsi rekursif sederhana, di mana pengulangan dilakukan di akhir fungsi, sehingga tidak ada kode yang dilakukan secara berlebihan, yang membantu sebagian besar kompiler bahasa pemrograman tingkat tinggi untuk melakukan apa yang dikenal sebagai Pengoptimalan Rekursi Ekor , juga memiliki pengoptimalan yang lebih kompleks yang dikenal sebagai modulo rekursi Tail
sumber
Fungsi rekursif adalah fungsi yang memanggil dengan sendirinya
Ini memungkinkan pemrogram untuk menulis program yang efisien menggunakan jumlah kode minimal .
Kelemahannya adalah bahwa mereka dapat menyebabkan loop tak terbatas dan hasil tak terduga lainnya jika tidak ditulis dengan benar .
Saya akan menjelaskan fungsi Rekursif Sederhana dan fungsi Rekursif Ekor
Untuk menulis fungsi rekursif sederhana
Dari contoh yang diberikan:
Dari contoh di atas
Apakah faktor penentu kapan harus keluar dari loop
Apakah pemrosesan sebenarnya harus dilakukan
Biarkan saya memecahkan tugas satu per satu agar mudah dipahami.
Mari kita lihat apa yang terjadi secara internal jika saya lari
fact(4)
If
loop gagal sehingga beralih keelse
loop sehingga kembali4 * fact(3)
Dalam memori tumpukan, kami punya
4 * fact(3)
Mengganti n = 3
If
loop gagal sehingga beralih keelse
loopjadi itu kembali
3 * fact(2)
Ingat kami menyebut `` 4 * fakta (3) ''
Output untuk
fact(3) = 3 * fact(2)
Sejauh ini tumpukan itu
4 * fact(3) = 4 * 3 * fact(2)
Dalam memori tumpukan, kami punya
4 * 3 * fact(2)
Mengganti n = 2
If
loop gagal sehingga beralih keelse
loopjadi itu kembali
2 * fact(1)
Ingat kami menelepon
4 * 3 * fact(2)
Output untuk
fact(2) = 2 * fact(1)
Sejauh ini tumpukan itu
4 * 3 * fact(2) = 4 * 3 * 2 * fact(1)
Dalam memori tumpukan, kami punya
4 * 3 * 2 * fact(1)
Mengganti n = 1
If
loop benarjadi itu kembali
1
Ingat kami menelepon
4 * 3 * 2 * fact(1)
Output untuk
fact(1) = 1
Sejauh ini tumpukan itu
4 * 3 * 2 * fact(1) = 4 * 3 * 2 * 1
Akhirnya, hasil dari fakta (4) = 4 * 3 * 2 * 1 = 24
The Tail Rekursi akan
If
loop gagal sehingga beralih keelse
loop sehingga kembalifact(3, 4)
Dalam memori tumpukan, kami punya
fact(3, 4)
Mengganti n = 3
If
loop gagal sehingga beralih keelse
loopjadi itu kembali
fact(2, 12)
Dalam memori tumpukan, kami punya
fact(2, 12)
Mengganti n = 2
If
loop gagal sehingga beralih keelse
loopjadi itu kembali
fact(1, 24)
Dalam memori tumpukan, kami punya
fact(1, 24)
Mengganti n = 1
If
loop benarjadi itu kembali
running_total
Output untuk
running_total = 24
Akhirnya, hasil dari fakta (4,1) = 24
sumber
Jawaban saya lebih merupakan tebakan, karena rekursi adalah sesuatu yang berkaitan dengan implementasi internal.
Dalam rekursi ekor, fungsi rekursif dipanggil di akhir fungsi yang sama. Mungkin kompiler dapat mengoptimalkan dengan cara di bawah ini:
Seperti yang Anda lihat, kami menutup fungsi asli sebelum iterasi berikutnya dari fungsi yang sama, jadi kami sebenarnya tidak "menggunakan" tumpukan.
Tetapi saya yakin jika ada destruktor yang dipanggil di dalam fungsi, maka pengoptimalan ini mungkin tidak berlaku.
sumber
Compiler cukup cerdas untuk memahami rekursi tail. Dalam kasus, ketika kembali dari panggilan rekursif, tidak ada operasi yang tertunda dan panggilan rekursif adalah pernyataan terakhir, termasuk dalam kategori rekursi ekor. Compiler pada dasarnya melakukan optimasi rekursi ekor, menghapus implementasi stack. Pertimbangkan kode di bawah ini.
Setelah melakukan optimasi, kode di atas diubah menjadi kode di bawah ini.
Ini adalah cara compiler melakukan Tail Recursion Optimization.
sumber