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

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

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

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) как насчет этого? В обоих случаях вызов self будет последним, что будет выполнено:

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

4 ответов


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

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

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

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

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

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

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

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


всех своих функций хвост рекурсивной.

никаких инструкций не осталось после самостоятельного вызова

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


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

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


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