1.3. Условия окончания рекурсии

Подобно любым циклическим алгоритмам рекурсивные подпрограммы могут приводить к своеобразному зацикливанию, когда в процессе последовательного перехода к нижнему уровню не выполняется условие, обеспечивающее нахождение элементарной подзадачи. Наиболее надежный способ обеспечить окончание рекурсивной процедуры – ввести в нее некоторый параметр, например n>0, и при каждом рекурсивном обращении задавать n-1, проверяя условие n=0 как это организовано в вышеприведенном примере.

Важно также сделать оценку максимальной глубины рекурсии чтобы убедиться, что она достаточно мала и не приводит к переполнению программного стека.

1.4. О целесообразности использования рекурсии

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

Рассмотрим в качестве такого неподходящего примера задачу вычисления чисел Фибоначчи, которые определяются следующим рекурсивным соотношением:

а) b0=0; b1=1;

б) bn=bn-1+bn-2.

В результате прямого переписывания этого алгоритма получаем изящную рекурсивную подпрограмму:

Листинг1.4

Function Fb(n:word):word;

begin

if n=0 then Fb:=0

else if n=1 then Fb:=1

else Fb:=Fb(n-1)+Fb(n-2)

end;

Однако ценность этой программы только в ее изяществе. Рассмотрим, например, как она будет работать при вызове F:=Fb(5). Каждое обращение к функции с n>1 приводит еще к двум активациям и число обращений растет как 2n-1. Дерево обращений для n=5 можно представить следующим образом:

Из этого представления видно, что не выполняется основное требование стратегии «разделяй и властвуй» – разбиение производится на зависимые подзадачи, решение которых при рекурсивной реализации многократно повторяются. Ясно, что такая программа практического интереса не представляет.

Числа Фибоначчи могут быть вычислены без рекурсии, с помощью более эффективной итерационной процедуры, используя стратегию «динамического программирования»:

Листинг1.5

Function Fb(n:word):word;

Var I,X,y,z:word;

begin if n=0 then Result:=0

else if n=1 then Result:=1 else

begin

x:=0; y:=1

for i:=2 to n do

begin

z:=x+y;

x:=y;

y:=z;

end end;

Result:=y;

end;

Здесь, согласно стратегии «динамического программирования» происходит запоминание в ячейках x,y решения двух предыдущих пересекающихся подзадач, и в результате нет повторов решения одинаковых под-подзадач их пересечения.

Подсчитайте сколько памяти и количество операций сравнения и сложения необходимо для вычисления b5, b10 и сравните с предыдущей программой.

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

Задача о Ханойской башне. Имеются три стержня s1,s2,s3. На первом из них нанизаны n дисков различных диаметров, образующих правильную пирамиду ­ чем выше расположен диск, тем меньше его диаметр. Требуется переместить всю башню на второй стержень, причем диски можно переносить по одному, нельзя помещать диск на диск меньшего диаметра, для промежуточного хранения можно использовать третий диск.

Тривиальная задача – башня из 1 диска, решается за один прием:

1) перенести диск с s1 на s2.

Элементарная подзадача – башня из двух дисков, решается в три приема: 1) перенести диск с s1 на s3

2) перенести диск с s1 на s2

  1. перенести диск с s3 на s2

Рекурсивное соотношение:

Башня из n дисков представляется как башня из 2-х – один нижний и верхний, состоящий из n-1 дисков.

Рекурсивная процедура записывается следующим образом:

Листинг1.6

Procedure Hanoi(n,s1,s2,s3:word);

Begin

If n>0 then

begin

Hanoi(n-1,s1,s3,s2);

печать(перенести диск со стержня s1 на стержень s2);

Hanoi(n-1,s3,s2,s1);

end;

end;

Обращение: Hanoi(n,1,2,3).

В результате при n=3 будет выдан следующий алгоритм перемещения трех дисков:

  1. перенести диск со стержня 1 на стержень2

  2. перенести диск со стержня 1 на стержень3

  3. перенести диск со стержня 2 на стержень3

  4. перенести диск со стержня 1 на стержень2

  5. перенести диск со стержня 3 на стержень1

  6. перенести диск со стержня 3 на стержень2

  7. перенести диск со стержня 1 на стержень2

Обращение к процедуре:Hanoi(3,1,2,3) вызовет следующее дерево активаций и печати: