Я почти понимаю, как работает хвостовая рекурсия и чем она отличается от обычной. Я только не понимаю, почему не требуется, чтобы стек запомнил свой адрес возврата.
// 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);
}
После вызова функции в функции хвостовой рекурсии делать нечего, но для меня это не имеет смысла.
c
algorithm
recursion
tail-recursion
Алан Коромано
источник
источник
-O3
. Ссылка предназначена для более раннего обсуждения, которое охватывает очень похожие темы и обсуждает, что необходимо для реализации этой оптимизации.Ответы:
Компилятор просто может преобразовать это
примерно так:
источник
Вы спросите, почему «стеку не требуется запоминать свой адрес возврата».
Я хотел бы все изменить. Это делает использование стека помнить обратный адрес. Хитрость в том, что функция, в которой происходит хвостовая рекурсия, имеет свой собственный адрес возврата в стеке, и когда она переходит к вызываемой функции, она будет рассматривать его как свой собственный адрес возврата.
Конкретно, без оптимизации хвостового вызова:
В этом случае при
g
вызове стек будет выглядеть так:С другой стороны, с оптимизацией хвостового вызова:
В этом случае при
g
вызове стек будет выглядеть так:Очевидно, что при
g
возврате он вернется в то место, откудаf
был вызван.EDIT : в приведенном выше примере используется случай, когда одна функция вызывает другую функцию. Механизм идентичен, когда функция вызывает сама себя.
источник
Хвостовая рекурсия обычно может быть преобразована компилятором в цикл, особенно при использовании аккумуляторов.
компилируется во что-то вроде
источник
В рекурсивной функции должны присутствовать два элемента:
«Обычная» рекурсивная функция сохраняет (2) в кадре стека.
Возвращаемые значения в обычной рекурсивной функции состоят из значений двух типов:
Посмотрим на ваш пример:
Кадр f (5) "хранит", например, результат своего собственного вычисления (5) и значение f (4). Если я вызываю factorial (5), непосредственно перед тем, как вызовы стека начинают сворачиваться, у меня есть:
Обратите внимание, что каждый стек хранит, помимо упомянутых мной значений, всю область действия функции. Итак, использование памяти для рекурсивной функции f равно O (x), где x - количество рекурсивных вызовов, которые мне нужно сделать. Итак, если мне нужен 1 КБ ОЗУ для вычисления факториала (1) или факториала (2), мне нужно ~ 100 КБ для вычисления факториала (100) и так далее.
Рекурсивная функция хвоста помещает (2) в свои аргументы.
В хвостовой рекурсии я передаю результат частичных вычислений в каждом рекурсивном кадре в следующий, используя параметры. Давайте посмотрим на наш факторный пример Tail Recursive:
int factorial (int n) {int helper (int num, int Накопленный) {если num == 0 возврат накопленного else return helper (num - 1, накопленный * num)} return helper (n, 1)
}
Посмотрим на его кадры в factorial (4):
Видите различия? В «обычных» рекурсивных вызовах возвращаемые функции рекурсивно составляют окончательное значение. В Tail Recursion они ссылаются только на базовый вариант (последний оцененный) . Мы называем аккумулятор аргументом, который отслеживает более старые значения.
Шаблоны рекурсии
Обычная рекурсивная функция выглядит следующим образом:
Чтобы преобразовать его в хвостовую рекурсию, мы:
Смотреть:
Увидеть разницу?
Оптимизация хвостового вызова
Поскольку в не-граничных случаях стеков хвостовых вызовов не сохраняется состояние, они не так важны. Некоторые языки / интерпретаторы затем заменяют старый стек новым. Итак, без фреймов стека, ограничивающих количество вызовов, хвостовые вызовы в этих случаях ведут себя так же, как цикл for .
Ваш компилятор должен оптимизировать его, или нет.
источник
Вот простой пример, показывающий, как работают рекурсивные функции:
Хвостовая рекурсия - это простая рекурсивная функция, в которой повторение выполняется в конце функции, поэтому код не выполняется по возрастанию, что помогает большинству компиляторов языков программирования высокого уровня выполнять то, что известно как оптимизация рекурсии хвоста , также имеет более сложная оптимизация, известная как хвостовая рекурсия по модулю
источник
Рекурсивная функция - это функция, которая вызывает сама по себе
Это позволяет программистам писать эффективные программы, используя минимальный объем кода .
Обратной стороной является то, что они могут вызвать бесконечные циклы и другие неожиданные результаты, если они не написаны должным образом .
Я объясню как простую рекурсивную функцию, так и хвостовую рекурсивную функцию.
Чтобы написать простую рекурсивную функцию
Из данного примера:
Из приведенного выше примера
Решающий фактор, когда выходить из цикла
Будет ли выполняться фактическая обработка
Позвольте мне разбить задачи по одному для облегчения понимания.
Посмотрим, что произойдет внутри, если я сбегу
fact(4)
If
цикл не работает, поэтому он переходит вelse
цикл, поэтому он возвращается4 * fact(3)
В стековой памяти у нас есть
4 * fact(3)
Подставив n = 3
If
цикл не работает, поэтому он переходит вelse
циклтак что он возвращается
3 * fact(2)
Помните, мы назвали `` 4 * факт (3) ''
Выход для
fact(3) = 3 * fact(2)
Пока в стеке
4 * fact(3) = 4 * 3 * fact(2)
В стековой памяти у нас есть
4 * 3 * fact(2)
Подставляя n = 2
If
цикл не работает, поэтому он переходит вelse
циклтак что он возвращается
2 * fact(1)
Помните, мы звонили
4 * 3 * fact(2)
Выход для
fact(2) = 2 * fact(1)
Пока в стеке
4 * 3 * fact(2) = 4 * 3 * 2 * fact(1)
В стековой памяти у нас есть
4 * 3 * 2 * fact(1)
Подставив n = 1
If
петля вернатак что он возвращается
1
Помните, мы звонили
4 * 3 * 2 * fact(1)
Выход для
fact(1) = 1
Пока в стеке
4 * 3 * 2 * fact(1) = 4 * 3 * 2 * 1
Наконец, результат факта (4) = 4 * 3 * 2 * 1 = 24
Хвостовая рекурсия будет
If
цикл не работает, поэтому он переходит вelse
цикл, поэтому он возвращаетсяfact(3, 4)
В стековой памяти у нас есть
fact(3, 4)
Подставив n = 3
If
цикл не работает, поэтому он переходит вelse
циклтак что он возвращается
fact(2, 12)
В стековой памяти у нас есть
fact(2, 12)
Подставляя n = 2
If
цикл не работает, поэтому он переходит вelse
циклтак что он возвращается
fact(1, 24)
В стековой памяти у нас есть
fact(1, 24)
Подставив n = 1
If
петля вернатак что он возвращается
running_total
Выход для
running_total = 24
Наконец, результат факта (4,1) = 24
источник
Мой ответ скорее предположительный, потому что рекурсия - это что-то, связанное с внутренней реализацией.
В хвостовой рекурсии рекурсивная функция вызывается в конце той же функции. Вероятно, компилятор может оптимизировать следующим образом:
Как видите, мы завершаем работу с исходной функцией перед следующей итерацией той же функции, поэтому на самом деле мы не «используем» стек.
Но я считаю, что если внутри функции вызываются деструкторы, эта оптимизация может не применяться.
источник
Компилятор достаточно умен, чтобы понимать хвостовую рекурсию. В случае, если при возврате из рекурсивного вызова нет ожидающей операции и рекурсивный вызов является последним оператором, подпадают под категорию хвостовой рекурсии. Компилятор в основном выполняет оптимизацию хвостовой рекурсии, удаляя реализацию стека. См. Код ниже.
После выполнения оптимизации приведенный выше код преобразуется в приведенный ниже.
Вот как компилятор выполняет оптимизацию рекурсии хвоста.
источник