Хвостовая рекурсия против прямой рекурсии

Может ли кто-то дать мне разницу между этими рекурсиями и примерами двух видов (в частности, в OCaml)?

Ответы

Ответ 1

Рекурсивная функция хвоста - это функция, в которой единственный рекурсивный вызов является последним в функции. Рекурсивная функция без хвоста - это функция, в которой это не так.

Обратная рекурсия - это рекурсия, где в каждом рекурсивном вызове значение параметра меньше, чем на предыдущем шаге. Прямая рекурсия - это рекурсия, где она увеличивается с каждым шагом.

Это два ортогональных понятия, то есть рекурсия вперед может быть или не быть хвостовой рекурсивной, и то же самое относится к обратным рекурсиям.

Например, факториальная функция часто написана так на императивных языках:

fac = 1
for i from 1 to n:
    fac := fac * i

Общая рекурсивная версия факториала отсчитывает назад (т.е. вызывает себя с n-1 в качестве параметра), однако, если вы непосредственно переводите вышеупомянутое императивное решение, вы бы предложили рекурсивную версию, которая подсчитывается вверх. Он будет выглядеть примерно так:

let fac n =
  let rec loop i =
    if i >= n
    then i
    else i * loop (i+1)
  in
    loop 1

Это прямая рекурсия, и, как вы можете видеть, она немного более громоздка, чем обратный рекурсивный вариант, поскольку для этого требуется вспомогательная функция. Теперь это не хвостовая рекурсия, поскольку последний вызов в loop - это умножение, а не рекурсия. Поэтому, чтобы сделать его хвостовым рекурсивным, вы сделали бы что-то вроде этого:

let fac n =
  let rec loop acc i =
    if i >= n
    then acc
    else loop (i*acc) (i+1)
  in
    loop 1 1

Теперь это и рекурсия вперед, и хвостовая рекурсия, потому что рекурсивный вызов: a) хвостовой вызов и b) вызывает себя с большим значением (i+1).

Ответ 2

Здесь приведен пример хвостовой рекурсивной факторной функции:

let fac n =
let rec f n a =
    match n with
    0 -> a
    | _ -> f (n-1) (n*a)
in
f n 1

Вот это не хвостовая рекурсивная копия:

let rec non_tail_fac n =
match n with
0 -> 1
| _ -> (non_tail_fac n-1) * n

Рекурсивная функция хвоста использует накопитель a для хранения значения результата предыдущего вызова. Это позволяет OCaml выполнять оптимизацию хвостового вызова, что приводит к тому, что стек не переполняется. Как правило, функция хвостовой рекурсии будет использовать значение аккумулятора для разрешения оптимизации хвостового вызова.

Ответ 3

Например, рекурсивная функция build_word, которая принимает char list и объединяет их со строкой, т.е. ['f'; 'o'; 'o'], в строку "foo". Процесс индукции можно визуализировать следующим образом:

build_word ['f'; 'o'; 'o']
"f" ^ (build_word ['o'; 'o'])
"f" ^ ("o" ^ (build_word ['o'])    // base case! return "o" and fold back
"f" ^ ("o" ^ ("o"))
"f" ^ ("oo")
"foo"

Это была нормальная рекурсия. Обратите внимание, что каждая пара круглых скобок означает новый стек стека или рекурсивный вызов. Решение этой проблемы (т.е. "F", "fo" или "foo" ) не может быть выведено до конца рекурсии (где выполняется базовый случай). Только тогда последний кадр возвращает последний результат назад к предыдущему, прежде чем "popping" и наоборот.

В теории каждый вызов создает новый стек стека (или область, если хотите), чтобы удерживать "место" для фрагментированного решения, которое нужно вернуть и собрать в начале. Это может привести к fooobar.com/info/4776/... (эта ссылка является рекурсивной btw).

Версия с хвостовым вызовом будет выглядеть примерно так:

build_word ['f'; 'o'; 'o'] ""
build_word ['o'; 'o'], "f"
build_word ['o'] ("f" ^ "o")
build_word [] ("f" ^ "o" ^ "o")
"foo"

Здесь накопленный результат (часто хранящийся в переменной, известной как accumulator), передается вперед. При оптимизации хвост-вызов не должен был бы создавать новый стек кадров, поскольку он не должен поддерживать предыдущие. Решение решается "вперед", а не "назад".

Вот функции build_word в двух версиях:

не-хвост

let build_word chars = 
  match chars with
  | [] -> None
  | [c] -> Some Char.to_string c
  | hd :: tl -> build_word tl
;;

хвост

let build_word ?(acc = "") chars =
  match chars with
  | [] -> None
  | [c] -> Some Char.to_string c
  | hd::tl -> build_word ~acc:(acc ^ Char.to_string hd) tl
;;

Прямая рекурсия хорошо объясняется в принятом ответе @sepp2k.