Рекурсия

Определение и виды рекурсии

Рекурсия — это такая организация выполнения работы функции, при которой данная функция вызывает сама себя.

Рекурсия может быть прямой и косвенной.

В случае прямой рекурсии вызов функцией самой себя делается непосредственно в этой же функции:

void f() {

............

f();

............

}

Косвенная рекурсия создаётся за счёт вызова данной функции из какой-либо другой функции, которая сама вызывалась из данной функции. Например, схема может быть такой:


Как видим, в main() вызывается функция A(), которая вызывает B(), а та в свою очередь вызывает снова A(). Понятно, что цепочка подпрограмм между вызовами функции A() могла быть не из одной функции B(), а из большего количества разных функции.

Стандартом языка C++ рекурсивно может вызываться любая функция, кроме main(), а по стандарту языка C рекурсивно можно вызвать даже main()!

Практика показывает, что функцию main() в ряде систем разработки (CodeBlocks, например) можно вызвать на любом языке (и на C, и на C++):

1)Прямая рекурсия функции main() на языке C++:

#include <iostream>

using namespace std;

int main()

{

   cout << "Hello world!" << endl;

   main();

   return 0;

}

2)Косвенная рекурсия функции main() на языке C++:

#include <iostream>

using namespace std;

void f();

int main()

{

   cout << "Hello world!" << endl;

   f();

   return 0;

}

void f()

{

   main();

}

В обоих примерах на экране монитора достаточно долго будет выводится радостный текст

Hello world!

а затем произойдёт аварийное завершение программы из-за переполнения стека.

Не зависимо от того, какой вид рекурсии используется (прямая или косвенная), в рекурсивной функции обязательно должна выполняться проверка какого-либо условия, в зависимости от которого осуществлялся бы выход из рекурсивной функции. Причём, необходимо гарантировать, что это условие обязательно будет выполнено через конечное количество рекурсивных вызовов, и функция в конце концов закончит работу. Иначе — аварийное завершение, смотри примеры выше.

Последовательный вызов функцией самой себя называют рекурсивным спуском, последовательный выход из многократного вызова — рекурсивным подъёмом.

При каждом рекурсивном вызове функции заново выделяется память под локальные переменные.

Рассмотрим примеры

Пример 1. Вычислить факториал n!.

Задача проста, но для понимания рекурсии вполне пригодна. По определению, n! — это произведение первых n натуральных чисел.

Если известен, к примеру, 4!, то 5! вычисляем по формуле: 5!=5·4!.

В свою очередь

4!=4·3!

3!=3·2!

2!=2·1!

а 1!=1

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

#include <iostream>

using namespace std;

int factorial(int n);

int main()

{

   int n = 5;

   int y = factorial(n);

   cout << n<<"! =" << y << endl;

   return 0;

}

int factorial(int n)

{

   int t;

   if(n <=1)

      t = 1;

   else

      t = n * factorial(n - 1);

   return t;

}

При рекурсивном спуске n последовательно уменьшается на 1. Когда n=1, начинается рекурсивный подъём. Именно на подъёме начинают последовательно вычисляться 2!, 3!, 4!, …, n!.

Условие

if(n <=1)

и уменьшение n при обращении к factorial() в операторе

t = n * factorial(n - 1);

гарантируют защиту от рекурсивного зацикливания.

Пример 2. Для данного n вычислить число Фибоначчи, используя зависимость

F(0)=F(1)=1, F(N)=F(N-1)+F(N-2) при n>1.

Возможный вариант реализации:

#include <iostream>

using namespace std;

int Fib(int n);

int main()

{

   int n = 5;

   int y = Fib(n);

   cout << "Fib(" << n << ")=" << y<< endl;

   return 0;

}

int Fib(int n)

{

   if(n < 1)

   return 1;

   else

      return Fib(n-1) + Fib(n-2);

}

Если посмотреть рисунок ниже со схемой рекурсивного вызова функций Fib(), то нетрудно заметить, что мы многократно вычисляем одно и то же, например, Fib(2) вызывается трижды. Понятно, что такая расточительность будет заметно снижать скорость выполнения программы.


Пример 3. Вычислить функцию Аккермана для заданных неотрицательных целых чисел n и m по формулам:

A(n,m) = m+1, если n=0;

A(n,m) = A(n-1, 1), если n>0, m=0;

A(n,m) = A(n-1, A(n,m-1)), если n>0, m>0.

Реализация получается предельно проста, а скорость работы — предельно долгой даже при небольших n и m. К тому же здесь вероятно переполнение стека из-за катастрофически огромного количества рекурсивных вызовов. Попробуйте вручную посмотреть схему вызова этой интереснейшей функции.

Возможный вариант реализации:

#include <iostream>

using namespace std;

int Ak(int n, int m);

int main()

{

   int n=2, m=2, y;

   y = Ak(n, m);

   cout << "A(" <<n << "," << m << ")=" << y << endl;

   return 0;

}

int Ak(int n, int m)

{

   int a;

   if(n == 0)

      a = m + 1;

   else if(n > 0 && m == 0)

      a = Ak(n - 1, 1);

   else

      a = Ak(n - 1, Ak(n, m - 1));

   return a;

}

Пример 4. Быстрая сортировка, изобретена в 60-х годах 20-го века. Варианты этой сортировки существуют для большинства современных языков программирования. В С++ она присутствует в STL (стандартная библиотека шаблонов).

Возможный вариант реализации:

#include <iostream>

using namespace std;

void QuickSort(int x[], int i1, int j1);

int M=0; // Количество вызовов функции QuickSort()

int NN=0;// Число перестановок

const int n = 17;

int main()

{

   int i;

   int x[n]={12,14,6,-6,4,11,9,54,1,15,32,6,5,8,2,21,1};

   cout << "Do Sort X:" << endl;

   for(i = 0; i < n; i++)

      cout << x[i] << " ";

   cout << endl;

   QuickSort(x, 0, n-1);

   cout << "Posle Sort X:" << endl;

   for(i = 0; i < n; i++)

      cout << x[i] << " ";

   cout << "\nM="<< M << " NN=" << NN<< endl;

   return 0;

}

void QuickSort(int x[], int i1, int j1)

{

   int c;

   M++;

   // Границы (левая и правая) сортируемого участка

   cout << i1 << " "<< j1 << endl;

   int i = i1, j = j1, b = (x[i1]+x[j1])/2;

   do

   {

      while(x[i] < b && i < j1)

         i++;

      while(x[j] > b && j > i1)

         j--;

      if(i < j)

      {

         cout << "Swap: x[" << i <<"]=" << x[i] <<" and x[" << j << "]=" << x[j] << endl;

         c = x[i];

         x[i] = x[j];

         x[j] = c;

         i++;

         j--;

         NN++;

      }

      else

         if(i == j)

         {

            i++;

            j--;

         }

   } while (i <= j);


   // Содержимое сортируемого массива после очередных перестановок для текущего вызова QuickSort

   cout << "--- x:" << endl;

   for(int ii = 0; ii < n; ii++)

      cout << x[ii] << " ";

   cout << endl;


   if(i1 < j)

      QuickSort(x, i1, j);

   if(i < j1)

      QuickSort(x, i, j1);

}

Здесь заведены вспомогательные переменные M и NN для оценки скорости работы метода. Также при каждом рекурсивном вызове выполняется вывод на экран монитора содержимое сортируемого массива и границы обрабатываемого при данном вызове участка. Сделано это с одной целью: дать возможность лучше понять механизм работы метода быстрой сортировки.

Какие выводы можно сделать из рассмотренных примеров? Рекурсия позволяет в ряде случаев создавать очень компактный код, особенно в том случае, когда при постановке задачи определение каких-либо понятий имеет явно рекурсивный вид (смотри функцию Аккермана). Но за малый размер исходного кода приходится расплачиваться снижением производительности.

Назад
На верх
Вперёд
Hosted by uCoz