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


1692

Поки починаю вчитися ліпше, я натрапив на термін хвіст-рекурсивний . Що це означає саме?


153
Для допитливих: і в той час, і в той час були в мові дуже давно. Поки був у користуванні давньоанглійською мовою; в той час як середньоанглійський розвиток в той час. Як сполучники, вони взаємозамінні за значенням, але поки не збереглися у стандартній американській англійській мові.
Філіп Бартузі

14
Можливо, пізно, але це досить гарна стаття про
хворобу

5
Однією з найбільших переваг ідентифікації хвостово-рекурсивної функції є те, що вона може бути перетворена в ітеративну форму і, таким чином, повторно реалізувати алгоритм від методу-стека-накладних. Можливо, хочеться відвідати відповідь від @Kyle Cronin та декількох інших нижче
KGhatak

Це посилання від @yesudeep - найкращий, найдокладніший опис, який я знайшов - lua.org/pil/6.3.html
Джефф Фішер

1
Чи може хтось мені сказати: Чи сортуйте та швидко сортуйте використовуйте хвостову рекурсію (ТРО)?
majurageerthan

Відповіді:


1718

Розглянемо просту функцію, яка додає перші N натуральних чисел. (наприклад sum(5) = 1 + 2 + 3 + 4 + 5 = 15).

Ось проста реалізація JavaScript, яка використовує рекурсію:

function recsum(x) {
    if (x === 1) {
        return x;
    } else {
        return x + recsum(x - 1);
    }
}

Якщо ви зателефонували recsum(5), це оцінить інтерпретатор JavaScript:

recsum(5)
5 + recsum(4)
5 + (4 + recsum(3))
5 + (4 + (3 + recsum(2)))
5 + (4 + (3 + (2 + recsum(1))))
5 + (4 + (3 + (2 + 1)))
15

Зверніть увагу, як повинен завершитися кожен рекурсивний дзвінок, перш ніж інтерпретатор JavaScript почне фактично виконувати роботу з обчислення суми.

Ось хвостово-рекурсивна версія тієї ж функції:

function tailrecsum(x, running_total = 0) {
    if (x === 0) {
        return running_total;
    } else {
        return tailrecsum(x - 1, running_total + x);
    }
}

Ось послідовність подій, які відбудуться, якби ви зателефонували tailrecsum(5)(що фактично було б tailrecsum(5, 0)через другий аргумент за замовчуванням).

tailrecsum(5, 0)
tailrecsum(4, 5)
tailrecsum(3, 9)
tailrecsum(2, 12)
tailrecsum(1, 14)
tailrecsum(0, 15)
15

У хвостово-рекурсивному випадку з кожною оцінкою рекурсивного виклику running_totalоновлення оновлюється.

Примітка: в оригінальній відповіді були використані приклади Python. Вони були змінені на JavaScript, оскільки інтерпретатори Python не підтримують оптимізацію хвостових викликів . Однак, хоча оптимізація хвостових викликів є частиною специфікації ECMAScript 2015 , більшість інтерпретаторів JavaScript не підтримує її .


32
Чи можу я сказати, що при хвостовій рекурсії остаточна відповідь обчислюється ОСТАНОЮ викликом методу? Якщо це НЕ хвостова рекурсія, для розрахунку відповіді потрібні всі результати для всіх методів.
chrisapotek

2
Ось додаток, в якому представлено кілька прикладів у Lua: lua.org/pil/6.3.html Може бути корисним і для цього! :)
yesudeep

2
Може хтось, будь ласка, вирішив питання chrisapotek? Мене бентежить, як tail recursionможна досягти мови, яка не оптимізує зворотні дзвінки.
Кевін Мередіт

3
@KevinMeredith "хвостова рекурсія" означає, що останнє твердження у функції є рекурсивним викликом тієї самої функції. Ви вірні, що немає сенсу робити це мовою, яка не оптимізує цю рекурсію. Тим не менш, ця відповідь дійсно показує концепцію (майже) правильно. Це було б чіткіше хвостовий дзвінок, якби пропущено "else:". Не змінило б поведінку, але покладе хвостовий виклик як незалежне твердження. Я подаю це як редагування.
ToolmakerSteve

2
Тож у python немає переваги, оскільки при кожному заклику до функції tailrecsum створюється нова рамка стека - так?
Quazi Irfan

707

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

У хвостовій рекурсії спочатку ви виконуєте свої обчислення, а потім виконуєте рекурсивний виклик, передаючи результати свого поточного кроку на наступний рекурсивний крок. Це призводить до того, що остання заява складається у формі (return (recursive-function params)). В основному, значення повернення будь-якого заданого рекурсивного кроку є таким же, як і значення повернення наступного рекурсивного виклику .

Наслідком цього є те, що як тільки ви будете готові виконати свій наступний рекурсивний крок, вам більше не потрібен поточний кадр стека. Це дозволяє здійснити деяку оптимізацію. Насправді, з відповідною письмовою компілятором, ви ніколи не повинні мати переповнення стека смішок з хвостом рекурсивного виклику. Просто використовуйте поточний кадр стека для наступного рекурсивного кроку. Я майже впевнений, що Лісп це робить.


17
"Я майже впевнений, що Ліпп це робить" - схема робить, але звичайний Лісп не завжди.
Аарон

2
@Daniel "В основному, значення повернення будь-якого заданого рекурсивного кроку є таким же, як і значення повернення наступного рекурсивного дзвінка". - Я не бачу цього аргументу, що відповідає дійсності для фрагмента коду, опублікованого Лоріном Хохштейном. Чи можете ви, будь ласка, докладно?
Geek

8
@Geek Це дійсно пізня відповідь, але це насправді вірно на прикладі Лорін Хохштайн. Розрахунок для кожного кроку проводиться перед рекурсивним викликом, а не після нього. В результаті кожна зупинка просто повертає значення безпосередньо з попереднього кроку. Останній рекурсивний виклик завершує обчислення, а потім повертає кінцевий результат немодифікований весь шлях назад вниз стеком викликів.
reirab

3
"Скала" робить, але вам потрібно вказати @tailrec для його виконання.
SilentDirge

2
"Таким чином, ви не отримаєте результат свого розрахунку, поки не повернетесь з кожного рекурсивного дзвінка." - можливо, я це неправильно зрозумів, але це не особливо стосується ледачих мов, де традиційна рекурсія - єдиний спосіб фактично отримати результат без виклику всіх рекурсій (наприклад, складання над нескінченним списком Булів з &&).
hasufell

205

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

while(E) { S }; return Q

де Eі Qє виразами, і Sце послідовність висловлювань, і перетворити його в хвостову рекурсивну функцію

f() = if E then { S; return f() } else { return Q }

Звичайно, E, Sі Qповинні бути визначені , щоб обчислити деякі цікаві значення за деякими змінним. Наприклад, циклічна функція

sum(n) {
  int i = 1, k = 0;
  while( i <= n ) {
    k += i;
    ++i;
  }
  return k;
}

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

sum_aux(n,i,k) {
  if( i <= n ) {
    return sum_aux(n,i+1,k+i);
  } else {
    return k;
  }
}

sum(n) {
  return sum_aux(n,1,0);
}

(Це "обгортання" хвостово-рекурсивної функції з функцією з меншими параметрами є загальною функціональною ідіомою.)


У відповіді @LorinHochstein я зрозумів, виходячи з його пояснення, що рекурсія хвоста повинна бути, коли рекурсивна частина слідує за "Return", однак у вашому випадку хвоста рекурсивна - ні. Ви впевнені, що ваш приклад правильно вважається хвостовою рекурсією?
CodyBugstein

1
@Imray Хвостово-рекурсивна частина - це оператор "return sum_aux" всередині sum_aux.
Кріс Конвей

1
@lmray: код Кріса по суті еквівалентний. Порядок тесту "if / then" та стиль обмежувального тесту ... якщо x == 0 проти if (i <= n) ... це не те, на що слід повісити. Справа в тому, що кожна ітерація передає свій результат наступній.
Тейлор

else { return k; }можна змінити наreturn k;
c0der

144

Цей уривок із книги « Програмування в Луї» показує, як зробити правильну хвостову рекурсію (у Луї, але вона має стосуватися і Ліспа) і чому це краще.

Хвіст виклику [хвіст рекурсії] є свого роду Goto , одягнений , як виклик. Хвостовий дзвінок відбувається, коли функція викликає іншого як останню дію, тому нічого іншого робити не має. Наприклад, у наступному коді виклик до gхвоста:

function f (x)
  return g(x)
end

Після fдзвінків gбільше нічого не має. У таких ситуаціях програмі не потрібно повертатися до функції виклику, коли викликана функція закінчується. Тому після хвостового дзвінка програмі не потрібно зберігати будь-яку інформацію про функцію виклику в стеку. ...

Оскільки правильний виклик хвоста не використовує місця для стеку, кількість обмежених викликів хвоста, які може здійснювати програма, не обмежується. Наприклад, ми можемо викликати наступну функцію з будь-яким числом як аргумент; він ніколи не переповнює стек:

function foo (n)
  if n > 0 then return foo(n - 1) end
end

... Як я вже говорив раніше, хвостовий дзвінок - це свого роду гото. Таким чином, досить корисне застосування належних хвостових дзвінків у Lua - це програмування державних машин. Такі програми можуть представляти кожен стан функцією; змінити стан - це перейти до (або зателефонувати) певної функції. Як приклад, розглянемо просту гру в лабіринт. У лабіринті є кілька кімнат, кожна з яких має до чотирьох дверей: північна, південна, східна та західна. На кожному кроці користувач вводить напрямок руху. Якщо в цьому напрямку є двері, користувач іде до відповідної кімнати; в іншому випадку програма друкує попередження. Мета - перейти від початкової кімнати до кінцевої.

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

function room1 ()
  local move = io.read()
  if move == "south" then return room3()
  elseif move == "east" then return room2()
  else print("invalid move")
       return room1()   -- stay in the same room
  end
end

function room2 ()
  local move = io.read()
  if move == "south" then return room4()
  elseif move == "west" then return room1()
  else print("invalid move")
       return room2()
  end
end

function room3 ()
  local move = io.read()
  if move == "north" then return room1()
  elseif move == "east" then return room4()
  else print("invalid move")
       return room3()
  end
end

function room4 ()
  print("congratulations!")
end

Отже, ви бачите, коли ви здійснюєте рекурсивний дзвінок, як:

function x(n)
  if n==0 then return 0
  n= n-2
  return x(n) + 1
end

Це не є рекурсивним хвостом, тому що ви все ще маєте робити (додайте 1) у цій функції після здійснення рекурсивного дзвінка. Якщо ви введете дуже велике число, це, ймовірно, спричинить переповнення стека.


9
Це відмінна відповідь, оскільки він пояснює значення хвостових викликів на розмір стека.
Ендрю Лебедь

@AndrewSwan Дійсно, хоча я вважаю, що оригінальний запитувач і випадковий читач, який може спіткнутися з цим питанням, могли б краще подати прийняту відповідь (оскільки він може не знати, що насправді є стеком.) До речі, я використовую Джиру, великий вентилятор.
Гофман

1
Моя улюблена відповідь також через те, що включати значення для розміру стека.
njk2015

80

Використовуючи регулярну рекурсію, кожен рекурсивний виклик висуває інший запис на стек викликів. Коли рекурсія завершена, додаток повинен вискакувати кожен запис повністю назад.

З хвостовою рекурсією, залежно від мови, компілятор, можливо, зможе згорнути стек до одного запису, тому ви заощадите простір стека ... Великий рекурсивний запит може насправді викликати переповнення стека.

В основному Хвостові рекурсії можуть бути оптимізовані до ітерації.


1
"Великий рекурсивний запит може насправді викликати переповнення стека." має бути в першому абзаці, а не у другому (хвоста рекурсія)? Велика перевага хвостової рекурсії полягає в тому, що вона може бути оптимізована (наприклад: Схема) таким чином, щоб не "накопичувати" дзвінки в стеку, тому в основному уникнете переповнення стека!
Олів'є

69

У файлі жаргону слід сказати про визначення хвостової рекурсії:

хвостова рекурсія / н./

Якщо ви вже не хворі на це, дивіться рекурсію хвоста.


68

Замість того, щоб пояснювати це словами, ось приклад. Це схематична версія факторної функції:

(define (factorial x)
  (if (= x 0) 1
      (* x (factorial (- x 1)))))

Ось версія факторіалу, яка є рекурсивною:

(define factorial
  (letrec ((fact (lambda (x accum)
                   (if (= x 0) accum
                       (fact (- x 1) (* accum x))))))
    (lambda (x)
      (fact x 1))))

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


4
+1 для згадування найважливішого аспекту хвостових рекурсій, що вони можуть бути перетворені в ітеративну форму і тим самим перетворити її у форму складності пам'яті O (1).
КГатак

1
@KGhatak не зовсім; відповідь правильно говорить про "постійний простір стеків", а не пам'ять взагалі. щоб не запозичити, аби переконатися, що немає непорозумінь. наприклад, процедура рекурсивного списку-мутація хвоста list-reverseвиконуватиметься в постійному просторі стека, але створить і зросте структуру даних на купі. Додатковий аргумент для обходу дерев може використовувати модельований стек. і т. д.
Буде Несс

45

Хвостова рекурсія стосується рекурсивного виклику, що є останнім в останній інструкції з логіки в рекурсивному алгоритмі.

Зазвичай у рекурсії у вас є базовий регістр, який зупиняє рекурсивні дзвінки і починає вискакувати стек викликів. Щоб використати класичний приклад, хоча і більше C-ish, ніж Лісп, факторіальна функція ілюструє хвостову рекурсію. Рекурсивний виклик відбувається після перевірки стану базового випадку.

factorial(x, fac=1) {
  if (x == 1)
     return fac;
   else
     return factorial(x-1, x*fac);
}

Початковим викликом факторіалу було б factorial(n)де fac=1(значення за замовчуванням) і n - число, для якого слід розраховувати факторіал.


Я знайшов ваше пояснення найпростішим для розуміння, але якщо все-таки потрібно пройти, то хвостова рекурсія корисна лише для функцій з одним базовим випадком заяви. Розглянемо такий метод, як цей postimg.cc/5Yg3Cdjn . Примітка: зовнішній else- це крок, який можна назвати "базовим регістром", але проходить через кілька рядків. Я вас нерозумію чи моє припущення правильне? Рекурсія хвоста корисна лише для одного лайнера?
Я хочу відповіді

2
@IWantAnswers - Ні, тіло функції може бути довільно великим. Все, що потрібно для хвостового дзвінка, - це те, що гілка, в якій вона знаходиться, викликає функцію як останнє, що вона робить, і повертає результат виклику функції. factorialПриклад просто класичний простий приклад, це все.
TJ Crowder

28

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

Я написав допис у блозі на цю тему, в якому є графічні приклади того, як виглядають кадри стека.


21

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

Дуже простий і зрозумілий для розуміння.

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

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

public static int factorial(int mynumber) {
    if (mynumber == 1) {
        return 1;
    } else {            
        return mynumber * factorial(--mynumber);
    }
}

public static int tail_factorial(int mynumber, int sofar) {
    if (mynumber == 1) {
        return sofar;
    } else {
        return tail_factorial(--mynumber, sofar * mynumber);
    }
}

3
0! є 1. Отже, "mynumber == 1" має бути "mynumber == 0".
полерто

19

Найкращий спосіб для мене зрозуміти tail call recursion- це особливий випадок рекурсії, коли останній виклик (або хвіст) - сама функція.

Порівнюючи приклади, надані в Python:

def recsum(x):
 if x == 1:
  return x
 else:
  return x + recsum(x - 1)

^ РЕКУРСІЯ

def tailrecsum(x, running_total=0):
  if x == 0:
    return running_total
  else:
    return tailrecsum(x - 1, running_total + x)

^ ХВИЛЬНИЙ РЕКУРСІЯ

Як ви бачите в загальній рекурсивній версії, остаточний виклик у блоці коду є x + recsum(x - 1). Отже після виклику recsumметоду, є ще одна операція, яка є x + ...

Однак у хвостовій рекурсивній версії кінцевий виклик (або хвостовий виклик) у кодовому блоці є tailrecsum(x - 1, running_total + x) це означає, що останній виклик робиться самому методу і після цього немає жодної операції.

Цей момент важливий, тому що рецидивація хвоста, як показано тут, не змушує пам'ять зростати, тому що, коли основний VM бачить функцію, що викликає себе в хвостовому положенні (останній вираз, який слід оцінити у функції), він усуває поточний кадр стека, який відомий як оптимізація виклику хвостів (TCO).

EDIT

NB. Майте на увазі, що приклад вище написаний на Python, час виконання якого не підтримує TCO. Це лише приклад для пояснення суті. TCO підтримується такими мовами, як Scheme, Haskell тощо


12

На Java, ось можлива хвостова рекурсивна реалізація функції Фібоначчі:

public int tailRecursive(final int n) {
    if (n <= 2)
        return 1;
    return tailRecursiveAux(n, 1, 1);
}

private int tailRecursiveAux(int n, int iter, int acc) {
    if (iter == n)
        return acc;
    return tailRecursiveAux(n, ++iter, acc + iter);
}

Порівнюйте це зі стандартною рекурсивною реалізацією:

public int recursive(final int n) {
    if (n <= 2)
        return 1;
    return recursive(n - 1) + recursive(n - 2);
}

1
Це повертає неправильні результати для мене: для введення 8 я отримую 36, це має бути 21. Я щось пропускаю? Я використовую java і копію вставляю.
Альберто Закканні

1
Це повертає SUM (i) для i в [1, n]. Нічого спільного з Фібоначчі. Для Fibbo, вам потрібні тести , які віднімають iterдо accколи iter < (n-1).
Асколейн

10

Я не програміст Ліспа, але думаю, що це допоможе.

В основному це такий стиль програмування, що рекурсивний дзвінок - це останнє, що ви робите.


10

Ось загальний приклад Lisp, який робить факторіали, використовуючи хвостові рекурсії. Зважаючи на відсутність стику, можна було виконати шалено великі факторні обчислення ...

(defun ! (n &optional (product 1))
    (if (zerop n) product
        (! (1- n) (* product n))))

А потім для розваги можна спробувати (format nil "~R" (! 25))


9

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

Отже, це хвостова рекурсія, тобто N (x - 1, p * x) - це останнє твердження у функції, де компілятор розумний, щоб зрозуміти, що він може бути оптимізований до циклу for (циклічного). Другий параметр p несе проміжне значення продукту.

function N(x, p) {
   return x == 1 ? p : N(x - 1, p * x);
}

Це не рекурсивний спосіб написання вищевказаної функціональної функції (хоча деякі компілятори C ++ можуть у будь-якому випадку оптимізувати її).

function N(x) {
   return x == 1 ? 1 : x * N(x - 1);
}

але це не:

function F(x) {
  if (x == 1) return 0;
  if (x == 2) return 1;
  return F(x - 1) + F(x - 2);
}

Я написав довгий пост під назвою " Розуміння рекурсії хвоста - Visual Studio C ++ - Перегляд збірки "

введіть тут опис зображення


1
Яка функція N хвостово-рекурсивна?
Фабіан Піцке

N (x-1) - це останнє твердження у функції, де компілятор розумний, щоб зрозуміти, що його можна оптимізувати до циклу for (циклічний)
doctorlai

Мене хвилює те, що ваша функція N - це саме функція recsum з прийнятої відповіді на цю тему (за винятком того, що це сума, а не добуток), і що, як кажуть, recsum не є рекурсивним?
Фабіан Піцке

8

ось версія Perl 5 tailrecsumзгаданої раніше функції.

sub tail_rec_sum($;$){
  my( $x,$running_total ) = (@_,0);

  return $running_total unless $x;

  @_ = ($x-1,$running_total+$x);
  goto &tail_rec_sum; # throw away current stack frame
}

8

Це уривок із структури та інтерпретації комп’ютерних програм про хвостову рекурсію.

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

Однією з причин того, що відмінність між процесом і процедурою може бути заплутаною, є те, що більшість реалізацій загальних мов (включаючи Ada, Pascal і C) розроблені таким чином, що інтерпретація будь-якої рекурсивної процедури вимагає кількості пам'яті, яка зростає разом із кількість викликів процедур, навіть коли описаний процес, в принципі, є ітераційним. Як наслідок, ці мови можуть описувати ітераційні процеси, лише вдаючись до спеціальних "циклічних конструкцій", таких як "робити", "повторювати", "до", "" і "". Реалізація схеми не поділяє цей дефект. Він буде виконувати ітераційний процес у постійному просторі, навіть якщо ітераційний процес описаний рекурсивною процедурою. Реалізація з цим властивістю називається хвостово-рекурсивною. За допомогою хвостово-рекурсивної реалізації ітерація може бути виражена за допомогою звичайного механізму виклику процедури, так що спеціальні ітераційні конструкції корисні лише як синтаксичний цукор.


1
Я прочитав усі відповіді тут, і все ж це найяскравіше пояснення, яке стосується дійсно глибокого ядра цієї концепції. Це пояснюється так прямо, завдяки чому все виглядає так просто і настільки зрозуміло. Пробачте мою грубість, будь ласка. Це якось змушує мене відчувати, що інші відповіді просто не вдаряють цвяхом по голові. Я думаю, що саме тому SICP має значення.
englealuze

8

Рекурсивна функція - це функція, яка викликає сама по собі

Це дозволяє програмістам писати ефективні програми, використовуючи мінімальну кількість коду .

Мінус полягає в тому, що вони можуть спричинити нескінченні петлі та інші несподівані результати, якщо записані неправильно .

Я поясню як просту рекурсивну функцію, так і хвостову рекурсивну функцію

Для того, щоб написати просту рекурсивну функцію

  1. Перший момент, який слід врахувати, - це коли ви вирішите вийти з циклу, який є циклом if
  2. Друге - що робити, якщо ми є власною функцією

З наведеного прикладу:

public static int fact(int n){
  if(n <=1)
     return 1;
  else 
     return n * fact(n-1);
}

З наведеного прикладу

if(n <=1)
     return 1;

Чи є вирішальним фактором, коли вийти з циклу

else 
     return n * fact(n-1);

Чи має бути проведена фактична обробка

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

Давайте подивимося, що відбувається всередині, якщо я біжу fact(4)

  1. Підстановка n = 4
public static int fact(4){
  if(4 <=1)
     return 1;
  else 
     return 4 * fact(4-1);
}

Ifцикл виходить з ладу, тому він переходить до elseциклу, тому він повертається4 * fact(3)

  1. У пам'яті стека у нас є 4 * fact(3)

    Підстановка n = 3

public static int fact(3){
  if(3 <=1)
     return 1;
  else 
     return 3 * fact(3-1);
}

Ifцикл виходить з ладу, тому він переходить до elseциклу

тому воно повертається 3 * fact(2)

Пам'ятайте, ми називали `` 4 * факт (3) ``

Вихід для fact(3) = 3 * fact(2)

Поки що стек є 4 * fact(3) = 4 * 3 * fact(2)

  1. У пам'яті стека у нас є 4 * 3 * fact(2)

    Підстановка n = 2

public static int fact(2){
  if(2 <=1)
     return 1;
  else 
     return 2 * fact(2-1);
}

Ifцикл виходить з ладу, тому він переходить до elseциклу

тому воно повертається 2 * fact(1)

Пам'ятайте, ми телефонували 4 * 3 * fact(2)

Вихід для fact(2) = 2 * fact(1)

Поки що стек є 4 * 3 * fact(2) = 4 * 3 * 2 * fact(1)

  1. У пам'яті стека у нас є 4 * 3 * 2 * fact(1)

    Підстановка n = 1

public static int fact(1){
  if(1 <=1)
     return 1;
  else 
     return 1 * fact(1-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

введіть тут опис зображення

Хвостова рекурсія буде

public static int fact(x, running_total=1) {
    if (x==1) {
        return running_total;
    } else {
        return fact(x-1, running_total*x);
    }
}

  1. Підстановка n = 4
public static int fact(4, running_total=1) {
    if (x==1) {
        return running_total;
    } else {
        return fact(4-1, running_total*4);
    }
}

Ifцикл виходить з ладу, тому він переходить до elseциклу, тому він повертаєтьсяfact(3, 4)

  1. У пам'яті стека у нас є fact(3, 4)

    Підстановка n = 3

public static int fact(3, running_total=4) {
    if (x==1) {
        return running_total;
    } else {
        return fact(3-1, 4*3);
    }
}

Ifцикл виходить з ладу, тому він переходить до elseциклу

тому воно повертається fact(2, 12)

  1. У пам'яті стека у нас є fact(2, 12)

    Підстановка n = 2

public static int fact(2, running_total=12) {
    if (x==1) {
        return running_total;
    } else {
        return fact(2-1, 12*2);
    }
}

Ifцикл виходить з ладу, тому він переходить до elseциклу

тому воно повертається fact(1, 24)

  1. У пам'яті стека у нас є fact(1, 24)

    Підстановка n = 1

public static int fact(1, running_total=24) {
    if (x==1) {
        return running_total;
    } else {
        return fact(1-1, 24*1);
    }
}

If петля справжня

тому воно повертається running_total

Вихід для running_total = 24

Нарешті, результат факту (4,1) = 24

введіть тут опис зображення


7

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

Аналогія виходить з ладу, коли ви вважаєте, що деякі процеси можуть використовувати додаткові кадри, але все ще вважаються хвостовими рекурсивними, якщо стек не зростає нескінченно.


1
він не розбивається на розбитій інтерпретації розладу особистості :) Товариство розуму; Розум як суспільство. :)
Буде Несс

Оце Так! Тепер це ще один спосіб подумати над цим
sutanu dalui

7

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

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

Прямим підходом було б:

  factorial(n):

    if n==0 then 1

    else n*factorial(n-1)

Припустимо, ви називаєте факторіал (4). Дерево рекурсії буде:

       factorial(4)
       /        \
      4      factorial(3)
     /             \
    3          factorial(2)
   /                  \
  2                factorial(1)
 /                       \
1                       factorial(0)
                            \
                             1    

Максимальна глибина рекурсії у наведеному випадку - O (n).

Однак розглянемо наступний приклад:

factAux(m,n):
if n==0  then m;
else     factAux(m*n,n-1);

factTail(n):
   return factAux(1,n);

Дерево рекурсії для фактичного хвоста (4) буде:

factTail(4)
   |
factAux(1,4)
   |
factAux(4,3)
   |
factAux(12,2)
   |
factAux(24,1)
   |
factAux(24,0)
   |
  24

Тут також максимальна глибина рекурсії становить O (n), але жоден з викликів не додає додаткової змінної до стеку. Отже, компілятор може усунути стек.


7

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


6

Хвіст рекурсивна функція є рекурсивної функцією , де остання операція це робить , перш ніж Повертатися зробити виклик рекурсивної функції. Тобто повертається значення виклику рекурсивної функції негайно повертається. Наприклад, ваш код виглядатиме так:

def recursiveFunction(some_params):
    # some code here
    return recursiveFunction(some_args)
    # no code after the return statement

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

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

def factorial(number):
    if number == 1:
        # BASE CASE
        return 1
    else:
        # RECURSIVE CASE
        # Note that `number *` happens *after* the recursive call.
        # This means that this is *not* tail call recursion.
        return number * factorial(number - 1)

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

def factorial(number, accumulator=1):
    if number == 0:
        # BASE CASE
        return accumulator
    else:
        # RECURSIVE CASE
        # There's no code after the recursive call.
        # This is tail call recursion:
        return factorial(number - 1, number * accumulator)
print(factorial(5))

(Зверніть увагу, що, хоча це і є код Python, інтерпретатор CPython не робить оптимізацію хвостових викликів, тому впорядкування вашого коду таким чином не дає користі для виконання.)

Можливо, вам доведеться зробити свій код трохи нечитабельнішим, щоб скористатися оптимізацією хвостових викликів, як показано на прикладі факторії. (Наприклад, базовий випадок зараз трохи неінтуїтивний, і accumulatorпараметр ефективно використовується як своєрідна глобальна змінна.)

Але користь оптимізації хвостових викликів полягає в тому, що вона запобігає помилкам переповнення стека. (Зауважу, ви можете отримати ту саму перевагу, використовуючи ітеративний алгоритм замість рекурсивного.)

Переповнення стека виникають, коли на стек викликів було надто занадто багато об'єктів кадру. Об'єкт кадру висувається на стек виклику, коли викликається функція, і вискакує зі стеку викликів, коли функція повертається. Об'єкти фрейму містять інформацію, таку як локальні змінні та рядки коду для повернення, коли функція повертається.

Якщо ваша рекурсивна функція робить занадто багато рекурсивних дзвінків без повернення, стек виклику може перевищувати обмеження об'єкта кадру. (Кількість залежить від платформи; у Python за замовчуванням це 1000 об'єктів кадру.) Це викликає помилку переповнення стека . (Гей, звідси походить назва цього веб-сайту!)

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

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


"Рекурсія хвоста (також називається оптимізацією хвостового виклику або усуненням хвостового виклику)". Ні; усунення хвостових викликів або оптимізація хвостових викликів - це те, що ви можете застосувати до хвостово-рекурсивної функції, але вони не те саме. Ви можете записати хвостово-рекурсивні функції в Python (як ви показуєте), але вони не є більш ефективними, ніж не хвостово-рекурсивна функція, оскільки Python не виконує оптимізацію хвостових викликів.
чепнер

Чи означає це, що якщо комусь вдасться оптимізувати веб-сайт і зробити рекурсивний виклик хвостово-рекурсивним, ми б більше не мали сайту StackOverflow ?! Це жахливо.
Надіб Мамі

5

Щоб зрозуміти деякі основні відмінності між рекурсією виклику хвоста та рекурсією без виклику, ми можемо вивчити реалізацію .NET цих методів.

Ось стаття з деякими прикладами в C #, F # і C ++ \ CLI: Пригоди в хвості Рекурсія в C #, F # і C ++ \ CLI .

C # не оптимізується для рекурсії хвостового виклику, тоді як F # робить.

Принципові відмінності включають петлі проти обчислення Лямбда. C # розроблений з урахуванням циклів, тоді як F # побудований з принципів обчислення лямбда. Про дуже гарну (і безкоштовну) книгу про принципи обчислення Лямбди див. Структура та інтерпретація комп'ютерних програм від Абельсона, Суссмана та Суссмана .

Щодо хвостових дзвінків у F #, для дуже хорошої вступної статті дивіться детальний вступ до хвостових дзвінків у F # . Нарешті, ось стаття, яка висвітлює різницю між не хвостовою рекурсією та рекурсією виклику хвоста (у F #): Хвост-рекурсія проти нехвістової рекурсії у F різкій .

Якщо ви хочете ознайомитися з деякими відмінностями дизайну рекурсії хвостового виклику між C # і F #, перегляньте розділ Створення коду зворотного виклику в C # і F # .

Якщо вам достатньо турбуватися, щоб хотіти знати, які умови перешкоджають компілятору C # здійснювати оптимізацію хвостових викликів, див. Цю статтю: Умови JIT CLR хвоста .


4

Існує два основних типи рекурсій: рекурсія голови та хвоста.

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

У хвостовій рекурсивній функції всі обчислення відбуваються спочатку, а рекурсивний виклик - це останнє, що відбувається.

Взяті з цього супер дивовижного поста. Прочитайте, будь ласка, прочитайте його.


4

Рекурсія означає функцію, що викликає себе. Наприклад:

(define (un-ended name)
  (un-ended 'me)
  (print "How can I get here?"))

Хвост-Рекурсія означає рекурсію, яка завершує функцію:

(define (un-ended name)
  (print "hello")
  (un-ended 'me))

Дивіться, остання річ, що не закінчена функція (процедура, в жаргоні схеми) - це викликати себе. Ще один (більш корисний) приклад:

(define (map lst op)
  (define (helper done left)
    (if (nil? left)
        done
        (helper (cons (op (car left))
                      done)
                (cdr left))))
  (reverse (helper '() lst)))

У хелперній процедурі ОСТАННЕ, що це робиться, якщо ліва сторона не є нульовою, - це зателефонувати собі (ПІСЛЯ що-небудь мінує і щось cdr). Це, в основному, так, як відображати список.

Хвостова рекурсія має велику перевагу в тому, що інтерпретатор (або компілятор, залежно від мови та постачальника) може оптимізувати його та перетворити його на щось еквівалентне циклу часу. Власне кажучи, в традиціях Схеми більшість циклів "за" і "в той час" робиться в результаті хвостової рекурсії (наскільки я не знаю) немає і поки.


3

На це запитання є багато чудових відповідей ... але я не можу не впоратись із альтернативою, як визначити "хвостову рекурсію" або, принаймні, "правильну хвостову рекурсію". А саме: чи варто розглядати це як властивість певного виразу в програмі? Або варто розглядати це як властивість реалізації мови програмування ?

Докладніше про останній погляд є класичний папір Вілла Клінгера "Правильна рекурсія хвоста та ефективність простору" (PLDI 1998), яка визначала "правильну хвостову рекурсію" як властивість реалізації мови програмування. Визначення побудовано так, що дозволяє ігнорувати деталі реалізації (наприклад, чи дійсно стек викликів представлений через стек виконання або через пов'язаний список кадрів, виділений купою).

Для цього використовується асимптотичний аналіз: не часу виконання програми, як зазвичай, а скоріше використання простору програми . Таким чином, використання простору пов'язаного списку, виділеного купою, проти стека викликів під час виконання, закінчується асимптотично еквівалентним; тож слід ігнорувати цю деталь реалізації мови програмування (деталь, яка, безумовно, має значення на практиці, але може затуманити води трохи, коли намагаються визначити, чи відповідає дана реалізація вимозі бути "рекурсивною властивістю хвоста" )

Документ варто ретельно вивчити з кількох причин:

  • Це дає індуктивне визначення хвостових виразів та викликів хвоста програми. (Таке визначення, і чому такі дзвінки важливі, здається, є предметом більшості інших відповідей, наведених тут.)

    Ось ці визначення, лише щоб надати аромат тексту:

    Визначення 1 У хвостових виразах з програми , написаної на схемі ядер визначаються індуктивно наступним чином .

    1. Тіло лямбда-виразу - вираз хвоста
    2. Якщо (if E0 E1 E2)є виразом хвоста, то обидва E1і E2є виразами хвоста.
    3. Ніщо інше не є виразом хвоста.

    Визначення 2 хвіст виклик є хвіст вираження , яке є викликом процедури.

(хвостовий рекурсивний дзвінок або, як говориться в папері, "самохвост дзвінка" - це особливий випадок хвостового дзвінка, де процедура викликається самою собою.)

  • Вона забезпечує формальні визначення для шести різних «машин» для оцінки сердечника схеми, де кожна машина має таке ж спостерігається поведінка , за винятком для асимптотичного класу простору складності , що кожен знаходиться.

    Наприклад, після надання визначень для машин відповідно: 1. управління пам'яттю на основі стеків, 2. збирання сміття, але не викликає хвости, 3. збирання сміття та виклики хвоста, папір продовжує роботу з ще більш досконалими стратегіями управління сховищами, такими як 4. "ревізія хвоста evlis", де середовище не потрібно зберігати в ході оцінки останнього аргументу суб-вираження в хвостовому виклику; 5. зведення середовища закриття до лише вільних змінних цього закриття, і 6. так звана семантика "безпечного для космосу", визначена Аппелем та Шао .

  • Для того, щоб довести, що машини насправді належать до шести різних класів складності простору, у статті для кожної пари порівняних машин наводяться конкретні приклади програм, які піддаватимуть асимптотичний проміжок простору на одній машині, а не на іншій.


(Читаючи мою відповідь зараз, я не впевнений, чи вдалося насправді зафіксувати найважливіші моменти статті Клінгера . Але, на жаль, я не можу присвятити більше часу розробці цієї відповіді.)


1

Тут вже багато людей пояснили рекурсію. Я хотів би навести кілька міркувань про деякі переваги, які дає рекурсія у книзі Ріккардо Террелла «Конкурс у .NET, сучасні моделі паралельного та паралельного програмування»:

«Функціональна рекурсія є природним способом ітерації в ПП, оскільки вона уникає мутації стану. Під час кожної ітерації нове значення передається в конструктор циклу замість оновлення (мутації). Крім того, може бути складена рекурсивна функція, що робить вашу програму більш модульною, а також відкриває можливості для використання паралелізації ".

Ось також кілька цікавих приміток із тієї ж книги про рекурсію хвоста:

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

ПРИМІТКА Основна причина хвостового дзвінка як оптимізація - покращення локальності даних, використання пам'яті та використання кешу. Роблячи хвіст, виклик використовує той самий простір стека, що і абонент. Це знижує тиск пам'яті. Це незначно покращує кеш, тому що та ж пам’ять використовується повторно для наступних абонентів і може залишатися в кеші, а не видаляти старішу лінію кешу, щоб звільнити місце для нової лінії кеша.

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