На этом шаге мы рассмотрим особенности рекурсивной реализации такого алгоритма.
Итерационный подход может быть применён для создания рекурсивных хвостовых алгоритмов для многих задач. Рассмотрим более сложную задачу преобразования системы счисления. Представим себе итерационный алгоритм, выполняющий шаги по приведению десятичного числа 142 к основанию 5 (10325) согласно таблице 1.
n | b | p | s |
---|---|---|---|
142 | 5 | 1 | 0 |
28 | 5 | 10 | 2 |
5 | 5 | 100 | 32 |
1 | 5 | 1000 | 32 |
0 | 5 | 10000 | 1032 |
Помимо параметров, методу необходима переменная s, в которой будет храниться промежуточный результат, а в итоге – результат. Кроме того, дополнительная переменная p содержит степень 10, необходимую для обновления значения s. На каждом шаге n изменяется путём целочисленного деления на основание b (n // b), p умножается на 10, а переменная-накопитель s увеличивается на (n % b) * p. Этот итерационный алгоритм приведён в примере 11.5, где изначально p = 1 и s = 0. Наконец, когда n достигает значения 0, метод может возвратить результат, находящийся в s.
1 2 3 4 5 6 7 8 |
def decimal_to_base_iterative(n, b): s = 0 p = 1 while n > 0: s = s + (n % b) * p p = p * 10 n = n // b return s |
Аналогичная хвостовая рекурсивная функция должна моделировать итерации цикла вызовами самой себя. Переменные (и параметры) итерационной версии должны стать параметрами рекурсивных вызовов, а сама рекурсивная функция должна воспроизвести их обновления в теле итерационного цикла (строки 5-7). Наконец, начальное условие выполняется при n = 0, когда метод может вернуть значение переменной s, так как она будет содержать окончательный результат. В примере 11.6 приведена хвостовая рекурсивная функция вместе с методом-оболочкой, который инициализирует р единицей, а s - нулём. Обратите внимание на то, как аргументы рекурсивного вызова отражают изменение переменных итерационного алгоритма в теле цикла.
1 2 3 4 5 6 7 8 9 10 |
def decimal_to_base_tail(n, b, p, s): if n == 0: return s else: return decimal_to_base_tail(n // b, b, p * 10, s + (n % b) * p) def decimal_to_base_tail_wrapper(n, b): return decimal_to_base_tail(n, b, 1, 0) |
Хотя хвостовые рекурсивные функции в примерах 11.4 и 11.6, вне всяких сомнений, рекурсивны, такой способ их разработки соответствует парадигме императивного программирования, когда всё внимание сосредоточено на переменных и параметрах, определяющих состояние программы. Обратите внимание, что при разработке рекурсивного метода данный подход не опирается на декомпозицию задачи или индукцию. Вместо этого рекурсивные алгоритмы просто воспроизводят итерационные версии. По этой причине получаемые алгоритмы могут сбить с толку тех программистов, которые попытаются понять код с точки зрения декомпозиции задачи. Кроме того, из-за упомянутых на 162 шаге недостатков такой способ разработки, безусловно, следует избегать. (Для примеров 11.4 и 11.6 ошибки переполнения стека довольно редки, так как исходное значение факториала обычно небольшое, а высота дерева рекурсии для алгоритма преобразования к другому основанию является логарифмической функцией от n.) Таким образом, если к решению задачи применяется итерационный подход, то целесообразнее разрабатывать итерационный алгоритм.
На следующем шаге мы рассмотрим вложенную рекурсию.