Оптимизация Tail Call присутствует во многих языках и компиляторах. В этой ситуации компилятор распознает функцию вида:
int foo(n) {
...
return bar(n);
}
Здесь язык способен распознавать, что возвращаемый результат является результатом другой функции, и изменять вызов функции с новым кадром стека в переход.
Поймите, что классический метод факториала:
int factorial(n) {
if(n == 0) return 1;
if(n == 1) return 1;
return n * factorial(n - 1);
}
не является хвостовым вызовом, оптимизируемым из-за проверки, необходимой при возврате. ( Пример исходного кода и скомпилированного вывода )
Чтобы сделать этот хвостовой вызов оптимизируемым,
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);
}
Компиляция этого кода с помощью gcc -O2 -S fact.c
(-O2 необходима для включения оптимизации в компиляторе, но с большим количеством оптимизаций -O3 человеку становится трудно читать ...)
_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
( Пример исходного кода и скомпилированного вывода )
Можно увидеть в сегменте .L3
, jne
а не call
(который выполняет вызов подпрограммы с новым кадром стека).
Пожалуйста, обратите внимание, что это было сделано с C. Оптимизация вызовов Tail в Java сложна и зависит от реализации JVM (при этом я не видел ни одного, кто это делает, потому что это сложно и подразумевает необходимость требуемой модели безопасности Java, требующей стековых фреймов - это то, чего избегает TCO) - хвостовая рекурсия + java и хвостовая рекурсия + оптимизация - это хорошие наборы тегов для просмотра. Вы можете найти другие языки JVM способны оптимизировать хвостовую рекурсию лучше (попытка Clojure (который требует повторялся для оптимизации хвостового вызова), или Скале).
Это сказало,
Существует определенная радость от осознания того, что вы написали что-то правильно - идеальным способом, которым это можно сделать.
А теперь я собираюсь взять скотч и надеть немецкую электронику ...
К общему вопросу о "методах, позволяющих избежать переполнения стека в рекурсивном алгоритме" ...
Другой подход - включить счетчик рекурсии. Это больше для обнаружения бесконечных циклов, вызванных не зависящими от вас ситуациями (и плохим кодированием).
Счетчик рекурсии принимает форму
int foo(arg, counter) {
if(counter > RECURSION_MAX) { return -1; }
...
return foo(arg, counter + 1);
}
Каждый раз, когда вы делаете звонок, вы увеличиваете счетчик. Если счетчик становится слишком большим, вы выдаете ошибку (здесь просто возвращается -1, хотя в других языках вы можете предпочесть исключение). Идея состоит в том, чтобы предотвратить худшие вещи (из-за ошибок памяти) при выполнении рекурсии, которая намного глубже, чем ожидалось, и, вероятно, представляет собой бесконечный цикл.
Теоретически вам это не нужно. На практике я видел плохо написанный код, который поразил это из-за множества мелких ошибок и плохих практик кодирования (многопоточные проблемы параллелизма, когда что-то изменяет что-то вне метода, что заставляет другой поток входить в бесконечный цикл рекурсивных вызовов).
Используйте правильный алгоритм и решите правильную проблему. Похоже, что специально для гипотезы Коллатца вы пытаетесь решить ее способом xkcd :
Вы начинаете с числа и делаете обход дерева. Это быстро приводит к очень большому пространству поиска. Быстрый прогон для вычисления количества итераций для правильного ответа дает около 500 шагов. Это не должно быть проблемой для рекурсии с небольшим кадром стека.
Хотя знание рекурсивного решения не является плохой вещью, следует также понимать, что многократно итеративное решение лучше . Несколько способов приблизить преобразование рекурсивного алгоритма к итерационному можно увидеть на Stack Overflow в Way, чтобы перейти от рекурсии к итерации .