Рекурсивные методы
Рекурсивным называют метод, если он вызывает сам себя в качестве вспомогательного. В основе рекурсивного метода лежит так называемое "рекурсивное определение" какого-либо понятия. Классическим примером рекурсивного метода является метод, вычисляющий факториал.
Из курса математики известно, что 0!=1!=1, n!=1*2*3…*n. С другой стороны n!=(n-1)!*n. Таким образом, известны два частных случая параметра n, а именно n= 0 и n=1, при которых мы без каких-либо дополнительных вычислений можем определить значение факториала. Во всех остальных случаях, то есть для n>1, значение факториала может быть вычислено через значение факториала для параметра n-1. Таким образом, рекурсивный метод будет иметь вид:
{ static long F(int n) //рекурсивный метод { if (n==0 || n==1) return 1; //нерекурсивная ветвь else return n*F(n-1); //шаг рекурсии - повторный вызов метода с другим параметром } static void Main() { Console.Write("n="); int n =int.Parse( Console.ReadLine()); long f=F(n); //нерекурсивный вызов метода F Console.WriteLine("{0}!={1}",n, f); } }
Рассмотрим работу описанного выше рекурсивного метода для n=3.
Первый вызов метода осуществляется из метода Main, в нашем случае командой f=F(3). Этап вхождения в рекурсию обозначим жирными стрелками. Он продолжается до тех пор, пока значение переменной n не становится равной 1. После этого начинается выход из рекурсии (тонкие стрелки). В результате вычислений получается, что F(3)=3*2*1.
Рассмотренный вид рекурсии называют прямой. Метод с прямой рекурсией обычно содержит следующую структуру:
if (<условие>) <оператор>; else <вызов данного метода с другими параметрами>;
В качестве <условия> обычно записываются некоторые граничные случаи параметров, передаваемых рекурсивному методу, при которых результат его работы заранее известен, поэтому далее следует простой оператор или блок, а в ветви else происходит рекурсивный вызов данного метода с другими параметрами.
Что необходимо знать для реализации рекурсивного процесса? Со входом в рекурсию осуществляется вызов метода, а для выхода необходимо помнить точку возврата, т.е. то место программы откуда мы пришли и куда нам нужно будет возвратиться после завершения метода. Место хранения точек возврата называется стеком вызовов и для него выделяется определенная область оперативной памяти. В этом стеке запоминаются не только адреса точек возврата, но и копии значений всех параметров. По этим копиям восстанавливается при возврате вызывающий метод. При развертывании рекурсии за счет создания копий параметров возможно переполнение стека. Это является основным недостатком рекурсивного метода. С другой стороны, рекурсивные методы позволяют перейти к более компактной записи алгоритма.
Следует понимать, что любой рекурсивный метод можно преобразовать в обычный метод. И практически любой метод можно преобразовать в рекурсивный, если выявить рекуррентное соотношение между вычисляемыми в методе значениями.
Далее для сравнения каждую задачу будем решать с использованием обычного и рекурсивного методов:
Пример 1:Найти сумму цифр числа А.
Известно, что любое натуральное число A=an an-1... a1 a0, где an an-1... a1 a0 - цифры числа, можно представить следующим образом:
A=an an-1... a1 a0 = A=an*10n + an-1*10n-1 + ... a1*101 + a0*100 = ((...((an*10 + an-1)*10+ an-2)*10...)*10 + a1)*10 + a0
Например, число 1234 можно представить как:
1234 = 1*103 + 2*102 + 3*101 + 4*100 = ((1*10 + 2)*10 + 3)*10 + 4
Из данного представления видно, что получить последнюю цифру можно, если найти остаток от деления числа на 10. В связи с этим для разложения числа на составляющие его цифры можно использовать следующий алгоритм:
- Находим остаток при делении числа А на 10, т.е. получаем крайнюю правую цифру числа.
- Находим целую часть числа при делении A на 10, т.е. отбрасываем от числа A крайнюю правую цифру.
- Если преобразованное A > 0, то переходим на пункт 1. Иначе число равно нулю и отделять от него больше нечего.
Данный алгоритм будет использоваться при разработке нерекурсивного метода.
С другой стороны, сумму цифр числа 1234 можно представить следующим образом sum(1234)=sum(123)+4=(sum(12)+3)+4=(((sum(1)+2)+3)+4)=(((sum(0)+1)+2)+3)+4. Таким образом, если А=0, то сумма цифр числа также равна нулю, т.е. sum=0. В противном случае сумму цифр числа A можно представить рекуррентным соотношением sum(A)=sum(A/10)+A%10. Полученное рекуррентное соотношение будем использовать при разработке рекурсивного метода.
class Program { static long Sum(long a) //нерекурсивный метод { long sum=0; while (a>0) //пока a больше нуля { sum+=a%10; //добавляем к сумме последнюю цифру числа а a/=10; //отбрасываем от числа а последнюю цифру } return sum; //возвращаем в качестве результата сумму цифр числа a } static long SumR(long a) //рекурсивный метод { if (a==0) //если a =0, то return 0; // возвращаем 0 else return SumR(a/10)+ a%10; //иначе обращаемся к рекуррентному соотношению } static void Main() { Console.Write("n="); long n=long.Parse(Console.ReadLine()); Console.WriteLine("Нерекурсивный метод: "+Sum(n)); Console.WriteLine("Рекурсивный метод: "+SumR(n)); } } }
Пример 2:вычислить n-ный член последовательности Фиббоначи.
Первые два члена последовательности Фиббоначи равны 1, остальные получаются по рекуррентной формуле an=an-1+an-2.
class Program { static int Fb(int n) //нерекурсивный алгоритм { int a, a1=1, a2=1; if (n==1||n==2) return 1; else { for (int i=2; i<=n; ++i) { a=a1+a2; a1=a2; a2=a; } return a1; } } static int FbR(int n) //рекурсивный алгоритм { if (n==1 || n==2 )return 1; else return FbR(n-1)+FbR(n-2); } static void Main() { Console.Write("n="); int n=int.Parse(Console.ReadLine()); Console.WriteLine("Нерекурсивный метод: "+Fb(n)); Console.WriteLine("Рекурсивный метод: "+FbR(n)); } }
Рассмотренные выше рекурсивные методы возвращали некоторое значение, заданное рекуррентным соотношением. Однако, как мы знаем, не все методы возвращают значение. Кроме того, рассмотренные выше методы определяют простой вариант рекурсивного метода. В общем случае рекурсивный метод включает в себя некоторое множество операторов и один или несколько операторов рекурсивного вызова. Действия могут выполняться после рекурсивного вызова, до рекурсивного вызова, а также и до, и после рекурсивного вызова. Рассмотрим примеры "сложных" рекурсивных методов, не возвращающих значение.