Оптимизация хвостовой рекурсии выглядит следующим образом: в первом примере, поскольку вы не можете вычислить результат умножения return (n / p) * odds(n - 1, p - 1)
, пока не вызовете odds (n-1), оператор должен сохранить нашу текущую позицию в памяти (в стеке) и откройте новый вызов коэффициентов.
Рекурсивно, это произойдет и при следующем вызове, и после него, и так далее. Таким образом, у нас есть n ожидающих операций к тому времени, когда мы достигаем конца нашей рекурсии и начинаем возвращать значения и вычислять продукты.
Во втором примере, поскольку выполняемый оператор возврата просто return odds1(n - 1, p - 1, (n / p) * acc)
, мы можем вычислить аргументы функции и просто вызвать odds1 (n-1) без удержания нашей текущей позиции . Именно здесь происходит оптимизация, потому что теперь мне не нужно помнить, где я нахожусь каждый раз, когда открываю новый кадр в стеке.
Думайте об этом как о книжных ссылках. представьте, что вы открываете поваренную книгу и переходите к определенному рецепту, а ингредиенты перечислены следующим образом:
- соль
- ингредиенты на следующей странице
на следующей странице есть
- перец
- ингредиенты на следующей странице
и т.д.. как вы говорите, что все ингредиенты? Вы должны помнить, что вы видели на каждой странице!
хотя второй пример больше похож на следующий список ингредиентов:
- соль
- забудьте об этом, просто используйте то, что вы видите на следующей странице
следующая страница имеет:
- соль
- перец
- забудьте об этом, просто используйте то, что вы видите на следующей странице
и т.д.. к тому времени, когда вы дойдете до последней страницы (обратите внимание, что аналогия точна в том, что оба выполняют одинаковое количество вызовов функций), у вас есть все компоненты, без необходимости «держать в памяти» то, что вы видели на каждой странице, потому что это все там на последней странице!