Optimasi panggilan ekor hadir dalam banyak bahasa dan kompiler. Dalam situasi ini, kompiler mengenali fungsi dari form:
int foo(n) {
...
return bar(n);
}
Di sini, bahasa dapat mengenali bahwa hasil yang dikembalikan adalah hasil dari fungsi lain dan mengubah panggilan fungsi dengan bingkai tumpukan baru menjadi lompatan.
Sadarilah bahwa metode faktorial klasik:
int factorial(n) {
if(n == 0) return 1;
if(n == 1) return 1;
return n * factorial(n - 1);
}
adalah tidak ekor panggilan optimizatable karena pemeriksaan yang diperlukan pada kembali. ( Contoh kode sumber dan hasil kompilasi )
Untuk membuat panggilan ekor ini dapat dioptimalkan,
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);
}
Mengkompilasi kode ini dengan gcc -O2 -S fact.c
(-O2 diperlukan untuk mengaktifkan pengoptimalan dalam kompiler, tetapi dengan lebih banyak optimasi dari -O3 semakin sulit bagi manusia untuk membaca ...)
_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
( Contoh kode sumber dan hasil kompilasi )
Satu dapat melihat di segmen .L3
, jne
daripada call
(yang melakukan panggilan subrutin dengan bingkai tumpukan baru).
Harap dicatat ini dilakukan dengan C. Optimasi panggilan ekor di Jawa sulit dan tergantung pada implementasi JVM (yang mengatakan, saya belum melihat ada yang melakukannya, karena sulit dan implikasi dari model keamanan Java yang diperlukan yang memerlukan stack frames - yang harus dihindari TCO) - pengulangan-ekor + java dan pengulangan-ekor + optimisasi adalah kumpulan tag yang baik untuk dijelajahi. Anda mungkin menemukan bahasa JVM lain dapat mengoptimalkan rekursi ekor lebih baik (coba clojure (yang memerlukan pengulangan untuk mengoptimalkan panggilan pemanggilan), atau scala).
Yang mengatakan,
Ada kegembiraan tertentu karena mengetahui bahwa Anda menulis sesuatu dengan benar - dengan cara ideal yang bisa dilakukan.
Dan sekarang, saya akan mengambil scotch dan memakai electronica Jerman ...
Untuk pertanyaan umum "metode untuk menghindari tumpahnya tumpukan dalam algoritma rekursif" ...
Pendekatan lain adalah memasukkan counter rekursi. Ini lebih untuk mendeteksi loop tak terbatas yang disebabkan oleh situasi di luar kendali seseorang (dan kode yang buruk).
Counter rekursi mengambil bentuk
int foo(arg, counter) {
if(counter > RECURSION_MAX) { return -1; }
...
return foo(arg, counter + 1);
}
Setiap kali Anda melakukan panggilan, Anda menambah penghitung. Jika penghitung terlalu besar, Anda salah (di sini, hanya pengembalian -1, meskipun dalam bahasa lain Anda mungkin lebih suka membuang pengecualian). Idenya adalah untuk mencegah hal-hal buruk terjadi (kehabisan memori kesalahan) ketika melakukan rekursi yang jauh lebih dalam dari yang diharapkan dan kemungkinan loop tak terbatas.
Secara teori, Anda tidak perlu ini. Dalam praktiknya, saya telah melihat kode yang ditulis dengan buruk yang mengenai hal ini karena sejumlah besar kesalahan kecil dan praktik pengkodean yang buruk (masalah konkurensi multithreaded di mana sesuatu mengubah sesuatu di luar metode yang membuat utas lain masuk ke loop tak terbatas dari panggilan rekursif).
Gunakan algoritma yang tepat dan pecahkan masalah yang tepat. Khusus untuk dugaan Collatz, tampaknya Anda mencoba menyelesaikannya dengan cara xkcd :
Anda mulai dari angka dan melakukan traversal pohon. Ini dengan cepat mengarah ke ruang pencarian yang sangat besar. Jalankan cepat untuk menghitung jumlah iterasi untuk hasil jawaban yang benar dalam sekitar 500 langkah. Ini seharusnya tidak menjadi masalah untuk rekursi dengan bingkai tumpukan kecil.
Walaupun mengetahui solusi rekursif bukanlah hal yang buruk, kita juga harus menyadari bahwa berkali-kali solusi berulang lebih baik . Sejumlah cara mendekati konversi algoritma rekursif ke yang berulang dapat dilihat pada Stack Overflow di Way untuk beralih dari rekursi ke iterasi .