Как распознать то, что есть, а что нет хвостовой рекурсии?

Иногда это достаточно просто (если сам вызов - это последнее утверждение, это хвостовая рекурсия), но все же есть случаи, которые меня путают. Профессор сказал мне, что "если нет какой-либо инструкции для выполнения после самообзыскания, это рекурсия хвоста". Как насчет этих примеров (не обращайте внимания на то, что они не имеют большого смысла):

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

function foo(n)
{
    if(n == 0)
        return 0;
    else
        return foo(n-2);
}

b) Но как насчет этого? Это должен быть хвостовой вызов, потому что если условие истинно, ничего, кроме него, будет выполнено, но это не последнее утверждение?

function foo(n)
{
    if(n != 0)
        return foo(n-2);
    else
        return 0;
}

c) Как насчет этого? В обоих случаях самозапуск будет последним:

function foo(n)
{
    if(n == 0)
        return 0;
    else    
    {
        if(n > 100)
            return foo(n - 2);
        else
            return foo(n - 1);
    }
}

Ответ 1

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

Обычно, когда вызывается функция, вызывающий код будет хранить любые значения регистра, которые ему понадобятся позже, в стеке. Он также сохранит адрес возврата, указав следующую команду после вызова. Он будет делать все возможное, чтобы убедиться, что указатель стека настроен правильно для вызываемого абонента. Затем он перейдет к целевому адресу [*] (в данном случае, к той же функции). При возврате он знает, что возвращаемое значение находится в месте, указанном соглашением о вызове (слот регистрации или стека).

Для вызова хвоста вызывающий абонент не делает этого. Он игнорирует любые значения регистра, поскольку он знает, что они не понадобятся им позже. Он устанавливает указатель стека так, что вызываемый будет использовать тот же стек, который сделал вызывающий, и он не настроен как обратный адрес, он просто перескакивает на целевой адрес. Таким образом, вызываемый будет перезаписывать одну и ту же область стека, он поместит свое возвращаемое значение в том же месте, в котором вызывающий объект поместил бы его возвращаемое значение, а когда он вернется, он не вернется к своему вызывающему, но вернется к своему вызывающему абоненту вызывающий абонент.

Поэтому, неофициально, функция является хвостовой рекурсивной, когда возможно для оптимизации хвостового вызова, и когда целью хвостового вызова является сама функция. Эффект более или менее такой же, как если бы функция содержала цикл, и вместо того, чтобы называть себя, хвостовой вызов переходит к началу цикла. Это означает, что после вызова не должно быть никаких переменных (и, действительно, нет "работы", которая на языке С++ означает, что ничего не нужно разрушать), а возвращаемое значение хвостового вызова должно быть возвращено вызывающим.

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

int triangle(int n) {
    if (n == 0) return 0;
    return n + triangle(n-1);
}

можно сделать хвост-рекурсивным, либо программистом, либо автоматически с помощью достаточно умного компилятора, например:

int triangle(int n, int accumulator = 0) {
    if (n == 0) return accumulator;
    return triangle(n-1, accumulator + n);
}

Следовательно, прежняя функция может быть описана как "хвост рекурсивная" тем, кто говорит о достаточно умном языке/компиляторе. Будьте готовы к использованию этого варианта.

[*] Хранение обратного адреса, перемещение указателя стека и перескакивание могут или не могут быть завернуты в один код операции по архитектуре, но даже если это не так, что обычно происходит.

Ответ 2

Да; Я думаю, что ваш профессор имел в виду, что в любом случае, если конечная инструкция рекурсивна, то это хвостовая рекурсия.

Итак, все три примера являются хвостовыми рекурсивными.

Ответ 3

Все ваши функции рекурсивно хвоста.

нет инструкции после самообслуживания

означает: после самообслуживания вы возвращаетесь из функции, то есть больше не нужно выполнять код, а не то, что в функции больше нет строки кода.

Ответ 4

Все три примера являются хвостовыми рекурсивными. Вообще говоря, это хвостовая рекурсия, если результат функции (выражение, следующее за ключевым словом return) является одиночным вызовом самой функции. Ни один другой оператор не должен участвовать в самом внешнем уровне выражения. Если вызов сам по себе является лишь частью выражения, то машина должна выполнить вызов, но затем должна вернуться к оценке указанного выражения, то есть оно не было в хвосте выполнения функции, а в середине выражение. Однако это не относится к каким-либо параметрам, которые может принимать рекурсивный вызов: там разрешено все, включая рекурсивные вызовы к себе (например, "return foo (foo (0));" ). Разумеется, оптимизация вызовов для переходов возможна только для внешнего вызова.