Когда рекурсию использовать не нужно
Рекурсивные алгоритмы особенно подходят для задач, где обрабатываемые данные определяются в терминах рекурсии. Однако это не означает, что такое рекурсивное определение данных гарантирует бесспорность употребления для решения задачи рекурсивного алгоритма. Фактически объяснение концепций рекурсивных алгоритмов на неподходящих для этого примерах и вызвало широко распространенное предубеждение против использования рекурсий в программировании; их даже сделали синонимом неэффективности.
Программы, в которых следует избегать алгоритмической рекурсии, можно охарактеризовать некоторой схемой, отражающей их строение (специфично, что тут есть единственное обращение к Р в конце или начале всей конструкции):
Pº IF В THEN S; P END, (3.6)
Pº S; IF В THEN P END. (3.7)
Такие схемы естественны в ситуациях, где вычисляемые значения определяются с помощью простых рекуррентных отношений. Возьмем хорошо известный пример вычисления факториала
i=0,1,2,3,4,5,...,
fi=1, 1, 2, 6, 24, 120,....
Первое из чисел определяется явно – f0 = 1, а последующие определяются рекурсивным образом с помощью предшествующего числа:
Такое рекуррентное отношение предполагает рекурсивный алгоритм вычисления n-го факториального числа. Если мы введем две переменные i и f, обозначающие на i-м уровне рекурсии, то обнаружим, что для перехода к следующему числу последовательности нужно проделать такие вычисления:
i++; f*=i;
откуда получаем простую рекурсивную программу:
Однако более часто употребляется и полностью эквивалентна ей запись, где вместо Р приведена так называемая процедура-функция, т. е. процедура, с которой связывается значение–результат и которую поэтому можно применять прямо как составляющую выражения. В этом случае переменная F становится излишней, а роль i явно выполняет параметр процедуры:
PROCEDURE F (I: INTEGER): INTEGER; BEGIN
IF I > 0 THEN RETURN I*F(I-1) ELSE RETURN 1 END END F;
Теперь уже ясно, что в нашем примере рекурсия крайне просто заменяется итерацией. Это проделано в такой программе:
I := 0; F : = 1;
WHILE I < n DO I := 1+1; F := I*F END;
Конечно, существуют и более сложные схемы рекурсий, которые можно и необходимо переводить в итеративную форму. Возьмем в качестве примера вычисление чисел Фибоначчи, определяемых рекурсивным соотношением
fibn+1 =fibn + fibn–i для n > 0 (3.16)
и fib1 =1; fib0 =0.
Прямое, "наивное", переписывание приводит к рекурсивной программе:
PROCEDURE Fib(i: INTEGER): INTEGER;
BEGIN
IF n = 0 THEN RETURN 0
ELSIF n = 1 THEN RETURN 1 (3.17)
ELSE RETURN Fib(n-1)+Fib{n-2)
END
END Fib;
В
ычисление fib путем обращения к Fib(n) приводит к рекурсивным активациям этой процедуры-функции. Как часто это происходит? Каждое обращение с п > 1 приводит еще к двум обращениям, т. е. общее число вызовов растет экспоненциально (рис. 3.2).
Ясно, что такая программа практического интереса не представляет.
Однако, к счастью, числа Фибоначчи можно вычислять и по итеративной схеме, где с помощью вспомогательных переменных х = fibi и у = fibi-i удается избежать повторных вычислений одной и той же величины:
i = 1; х = 1; у = 0;
WHILE i < n DO z = х; х+ = y, у := z; i++ END;
И
так, урок таков: следует избегать рекурсий там, где есть очевидное итерационное решение. Однако это не означает, что от рекурсий следует избавляться любой ценой. Существует много хороших примеров применения рекурсии, что мы и продемонстрируем в последующем. То, что существует реализации рекурсивных процедур на фактически нерекурсивных машинах, доказывает, что для практических целей любую рекурсивную программу можно трансформировать в чисто итеративную. Однако это требует явной работы со стеком для рекурсий, причем эти действия часто настолько затуманивают суть программы, что ее бывает трудно понять. Вывод: алгоритмы, рекурсивные по своей природе, а не итеративные, нужно формулировать как рекурсивные процедуры.
