Рекурсивные программы
9.1. От циклов к рекурсии
Вернемся назад, к общим проблемам рекурсии.
Мы уже видели, что некоторые рекурсивные алгоритмы – числа Фибоначчи, вставка и поиск в бинарных деревьях – имеют циклические эквиваленты. Что можно сказать в общем случае?
Фактически, нетрудно заменить любой цикл рекурсией. Рассмотрим произвольный цикл, данный здесь без указания инвариантов и варианта (хотя позже мы познакомимся с рекурсивными двойниками):
from Init until Exit loop Body end
Мы можем заменить его на
Init loop_equiv Здесь введена процедура: loop_equiv — Используется условие выхода Exit и тело цикла Body. do if not Exit then Body Loop_equiv end end
В функциональных языках (таких как Lisp, Scheme, Haskell, ML) рекурсивный стиль является предпочитаемым, даже если доступны циклы. Мы могли бы также использовать рекурсию с первых шагов нашего курса, рассмотрев, например, анимацию линии метро, перемещающую красную точку, как рекурсивную процедуру:
Line8.start animate_rest (Line8) Вот как могла бы выглядеть сама процедура: animate_rest (line: LINE) — Анимация станций линии метро, начиная от текущей позиции курсора do if not line.after then show_spot (line.item.location) line.forth animate_rest (line) end end
(более полная версия должна восстанавливать текущую позицию курсора).
Рекурсивная версия выглядит элегантно, но нет особых причин в рамках нашего курса предпочитать ее форме с применением циклов, мы и в дальнейшем будем использовать циклы.
Но даже чисто теоретически важно понимать, что циклы не являются обязательной принадлежностью языка программирования и могут быть заменены рекурсией. Хорошим примером является программа paradox, демонстрирующая неразрешимость проблемы остановки. Рекурсия позволяет задать более выразительную версию:
recursive_paradox — Завершается, если и только если не завершается. do if terminates ("C:\your_project") then recursive_paradox end end
Знание того, что всякий цикл может быть заменен рекурсией, немедленно порождает вопрос, а верно ли обратное – можно ли рекурсивную процедуру заменить процедурой, использующей циклы?
С примерами замены мы уже встречались – те же числа Фибоначчи, has и put для бинарных деревьев. Другие рекурсивные процедуры – hanoi, height, print_all – не имели свободного от рекурсии эквивалента. Для понимания того, что точно может быть сделано, необходимо более глубоко познакомиться со свойствами и смыслом рекурсивных программ.
9.2. Понимание рекурсии
Приобретенный опыт построения рекурсивных программ позволяет нам более глубоко исследовать смысл рекурсивных определений.
Неправильные циклы?
Прежде всего, вернемся назад и зададим весьма невежливый вопрос: а не является ли рекурсия "голым королем"? Другими словами, стоит ли что-либо за рекурсивным определением? Примеры, особенно примеры рекурсивных программ, свидетельствуют в их пользу, но некоторая доля сомнений все же остается. Мы все же находимся в опасной близости к определениям, не имеющим смысла, – к каким-то неправильным циклам. Рекурсия позволяет определять понятие в терминах самого понятия. Но, когда говорится:
Информатика занимается изучением информатики
то это общее место, тавтология, ничего не определяющая, в отличие от тавтологий в логике, которые доказываются и, следовательно, представляют интерес. Можно попытаться улучшить определение, сказав:
Информатика занимается изучением программирования, структур данных, алгоритмов, приложений, теоретическими вопросами и другими областями информатики
В определение добавлены полезные элементы, но оно все еще не является удовлетворительным определением. Подобным образом могут оказаться бесполезными и рекурсивные программы, такие как эта:
p (x: INTEGER) — Что в этом хорошего? do p (x) end
Эта программа выполняется для любых целочисленных аргументов, не производя никакого результата, работая бесконечно долго.
Как избежать такого очевидного ошибочного использования рекурсии? Если попытаться понять, почему рекурсивные определения, с которыми мы сталкивались, кажутся интуитивно имеющими смысл, то можно выделить три интересных свойства.
Почувствуйте методологию
Полезное рекурсивное определение должно удовлетворять следующим требованиям:
Для рекурсивных программ изменение контекста (R2) может заключаться в том, что вызов использует различное значение аргумента, как в вызове r(n -1) в программе r(n:INTEGER). Этот вызов применим к различным целям – x.r(n), где x не является текущим объектом. Изменение контекста может также означать, что вызов встречается после того, как программа изменила по меньшей мере одно поле по меньшей мере одного объекта.
Все рекурсивные программы, рассмотренные нами ранее, удовлетворяют этим требованиям.
- Тело Hanoi(n, …) включает условный оператор if n > 0 then … end, где все рекурсивные вызовы сосредоточены в then-ветви оператора, но поскольку else-ветвь отсутствует, то эта "пустая" ветвь при n = 0 определяет нерекурсивный вариант (R1). Рекурсивные вызовы имеют форму Hanoi(n - 1, …), изменяя первый аргумент и порядок других аргументов (R2). Замена n на n – 1 приближает контекст к нерекурсивному случаю n = 0 (R3).
- Рекурсивный метод has для бинарных деревьев поиска имеет нерекурсивные варианты для x = item, для x < item, если нет левого поддерева, и для x > item, если нет правого поддерева (R1). Рекурсивные вызовы имеют другую цель – left или right, отличающуюся от текущего объекта (R2). Каждый такой вызов приближается к листьям дерева, где рекурсия заканчивается (R3). Все эти утверждения справедливы и для других методов, работающих с деревьями поиска, например, height.
- В методе animate_rest – рекурсивной версии обхода линии метро, – когда курсор находится в положении after, срабатывает нерекурсивная ветвь (R1), ничего не делающая. Рекурсивные вызовы не изменяют аргумент, но в процессе работы вызывается метод line.forth, изменяющий состояние линии (R2); при этом курсор передвигается ближе к состоянию after, где рекурсия заканчивается (R3).
Для рекурсивных понятий, не связанных с программами, условия R1, R2, R3 также должны выполняться.
- Мини-грамматика, определяющая понятие "Операторы", имеет нерекурсивный вариант – "Присваивание";
- Все наши рекурсивно определенные структуры данных, такие как STOP, являются рекурсивными благодаря ссылкам, которые могут иметь значение void. В связных структурах значения void служат в качестве терминаторов, завершающих структуру.
- В случае рекурсивных программ комбинирование трех вышеприведенных правил предполагает понятие варианта, подобное варианту цикла, гарантирующего завершение цикла.
Почувствуй методологию
Каждая рекурсивная программа должна быть объявлена с ассоциированным с рекурсией вариантом, целочисленной величиной, связанной с каждым вызовом, такой, что:
- предусловие программы гарантирует неотрицательность варианта;
- если выполнение программы начинается со значения v для варианта, то значение варианта v1 для любого рекурсивного вызова удовлетворяет условию 0 <= v1 < v.
Вариант может включать аргументы рекурсивного метода, а также другие элементы окружения, такие как атрибуты текущего объекта или другие объекты. Давайте посмотрим на примеры.
- Для Hanoi(n, …) вариантом является n.
- Для has, height, print_all и других рекурсивных методов, связанных с обходом бинарных деревьев, вариантом является node_height – наибольшая длина пути от текущего узла до одного из листьев дерева.
- Для animate_rest вариантом является, как и для соответствующего цикла, Line8.count – Line8.index +1.
Специального синтаксиса для вариантов рекурсивных методов нет, но мы будем использовать комментарий в следующей форме, показанной для процедуры Hanoi(n, …):
— variant n