Що таке хвостова рекурсія?


52

Я знаю загальну концепцію рекурсії. Я натрапив на концепцію хвостової рекурсії під час вивчення алгоритму швидкості. У цьому відео алгоритму швидкого сортування з MIT о 18:30 секунди професор говорить, що це хвостовий рекурсивний алгоритм. Мені не ясно, що насправді означає рекурсія хвоста.

Чи може хтось пояснити концепцію належним прикладом?

Деякі відповіді , представлені спільнотою SO тут .


Розкажіть більше про контекст, де ви стикалися з терміном рекурсія хвоста . Посилання? Цитування?
А.Шульц

@ A.Schulz Я поставив посилання на контекст.
Geek

5
Подивіться на " Що таке хвоста-рекурсія? " На stackoverflow
Vor

2
@ajmartin Питання є кордоном щодо переповнення стека, але чітко є темою з інформатики , тому в принципі комп'ютерна наука повинна дати кращі відповіді. Тут цього не сталося, але все-таки нормально просити сюди запитання в надії на кращу відповідь. Гек, ти мав би згадати своє попереднє запитання щодо ТА, щоб люди не повторювали сказане.
Жил "ТАК - перестань бути злим"

1
Крім того, ви повинні сказати, що є неоднозначною частиною або чому вас не влаштовують попередні відповіді, я думаю, що ТАК люди дають хороші відповіді, але що змусило вас знову запитати?

Відповіді:


52

Хвостова рекурсія - це особливий випадок рекурсії, коли функція виклику не робить більше обчислень після здійснення рекурсивного виклику. Наприклад, функція

int f (int x, int y) {
  якщо (у == 0) {
    повернути х;
  }

  повернення f (x * y, y-1);
}

є хвостовим рекурсивним (оскільки остаточна інструкція є рекурсивним викликом), тоді як ця функція не є хвостовою рекурсивною:

int g (int x) {
  якщо (x == 1) {
    повернути 1;
  }

  int y = g (x-1);

  повернути х * у;
}

оскільки він робить деякі обчислення після повернення рекурсивного дзвінка.

Рекурсія хвоста важлива, оскільки може бути здійснена ефективніше, ніж загальна рекурсія. Коли ми робимо звичайний рекурсивний виклик, нам потрібно натиснути зворотну адресу на стек виклику, а потім перейти до викликаної функції. Це означає, що нам потрібен стек викликів, розмір якого лінійний у глибині рекурсивних викликів. Коли у нас є хвоста-рекурсія, ми знаємо, що як тільки ми повернемося з рекурсивного дзвінка, ми також негайно повернемось, тому ми можемо пропустити весь ланцюг рекурсивних функцій, що повертаються і повертаються прямо до початкового абонента. Це означає, що нам взагалі не потрібен стек викликів для всіх рекурсивних дзвінків, і ми можемо реалізувати остаточний дзвінок як простий стрибок, що економить нам місце.


2
Ви писали "Це означає, що нам взагалі не потрібен стек викликів для всіх рекурсивних дзвінків". Стек викликів завжди буде там, тільки те, що зворотну адресу не потрібно записувати в стек викликів, правда?
Geek

2
Це певною мірою залежить від вашої моделі обчислень :) Але так, на реальному комп'ютері стек викликів все ще є, ми просто не використовуємо його.
Метт Льюїс

Що робити, якщо це остаточний дзвінок, але для циклу. Отже, ви робите всі ваші обчислення вище, але деякі з них є циклом for, наприкладdef recurse(x): if x < 0 return 1; for i in range 100{ (do calculations) recurse(x)}
thed0ctor

13

Простіше кажучи, хвоста рекурсія - це рекурсія, де компілятор міг би замінити рекурсивний виклик командою "goto", тому компільована версія не повинна збільшувати глибину стека.

Іноді для проектування хвостово-рекурсивної функції потрібно створити допоміжну функцію з додатковими параметрами.

Наприклад, це не хвостово-рекурсивна функція:

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

Але це хвоста-рекурсивна функція:

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

тому що компілятор може переписати рекурсивну функцію на нерекурсивну, використовуючи щось подібне (псевдокод):

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

Правило для компілятора дуже просте: коли ви знайдете " return thisfunction(newparameters);", замініть його на " parameters = newparameters; goto start;". Але це можна зробити, лише якщо значення, повернене рекурсивним викликом, повернеться безпосередньо.

Якщо всі рекурсивні виклики у функції можуть бути замінені так, то це хвоста-рекурсивна функція.


13

Моя відповідь ґрунтується на поясненні, поданому в книзі " Структура та інтерпретація комп'ютерних програм" . Я дуже рекомендую цю книгу комп'ютерним вченим.

Підхід A: Лінійний рекурсивний процес

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

Форма процесу для підходу A виглядає приблизно так:

(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

Підхід B: Лінійний ітеративний процес

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

Форма процесу для підходу B виглядає приблизно так:

(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

Лінійний ітеративний процес (підхід B) працює в постійному просторі, хоча процес є рекурсивною процедурою. Слід також зазначити, що в цьому підході набір змінних визначає стан процесу в будь-якій точці, а саме. {product, counter, max-count}. Це також техніка, за допомогою якої хвоста рекурсія дозволяє оптимізувати компілятор.

У підході A є більш прихована інформація, яку підтримує інтерпретатор, яка в основному є ланцюгом відкладених операцій.


5

Хвост-рекурсія - це форма рекурсії, в якій рекурсивні виклики є останніми інструкціями функції (саме звідси походить хвостова частина). Більше того, рекурсивний виклик не повинен складатися з посилань на комірки пам'яті, що зберігають попередні значення (посилання, крім параметрів функції). Таким чином, ми не дбаємо про попередні значення, і одного кадру стека достатньо для всіх рекурсивних викликів; хвостова рекурсія - це один із способів оптимізації рекурсивних алгоритмів. Інша перевага / оптимізація полягає в тому, що існує простий спосіб перетворення хвостово-рекурсивного алгоритму в еквівалентний, який використовує ітерацію замість рекурсії. Так, так, алгоритм кваксорбції дійсно є рекурсивним.

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

Ось ітеративна версія:

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
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.