§ 7. Понятие вспомогательного алгоритма

7.6. Рекурсия

Рекурсия — в программировании вызов подпрограммы (прямо или косвенно) из нее же самой. Количество вложенных вызовов подпрограммы называют глубиной рекурсии.

Различают прямую (простую, непосредственную) и косвенную (сложную, опосредованную) рекурсию.

Прямая рекурсия: при описании функции используется обращение к ней же самой, но с другим набором параметров.

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

Рекурсивная программа позволяет описать повторяющиеся действия без явных повторений частей программы и использования циклов.

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

Любая рекурсивная функция может быть реализована не рекурсивно.

Пример 7.14. Описать алгоритм Евклида с помощью рекурсивной функции.

Этапы выполнения задания

I. Исходные данные: два числа a, b.

II. Результат: НОД (a, b).

III. Алгоритм решения задачи.

1. Ввод исходных данных. 
2. Условием прекращения рекурсии будет равенство чисел. Если числа не равны, то вызываем саму функцию gcd. При этом один из параметров заменяем на разность. 
3. Вывод результата.

IV. Описание переменных: a, b, c, d — int.

Пример 7.15. Написать программу, которая выводит разложение числа на простые множители.

Этапы выполнения задания

I.     Исходные данные: число n.

II.     Результат: простые множители числа a.

III. Алгоритм решения задачи.

1. Ввод исходных данных.
2. 
Для получения разложения число нужно поочередно пытаться делить на простые числа: 2, 3, 5... . Если разделилось, то данное число входит в разложение и нужно попытаться разделить на него полученное частное, если нет, то попытаемся делить на следующее простое. Процесс заканчивается, когда в частном получаем 1.
3. 
Опишем рекурсивную функцию razlog(a, b).

3.1. Параметр а — число которое пытаемся делить, параметр b — число, на которое пытаемся делить. 
3.2. Условие прекращения рекурсии — параметр a равен 1. 
3.3. Если a делится на b, то при вызове функции первый параметр заменяем частным a / b, если нет, то второй параметр увеличиваем на 1. 
3.4. При такой организации вычислений можно не проверять, что текущий делитель является простым числом, поскольку ни на одно составное текущее значение первого параметра разделиться не сможет. 
3.5. Вывод результата будет осуществляться при возврате из рекурсии.

IV. Описание переменных: n — int

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

Пример 7.16. По определению . То есть для того, чтобы найти значение an необходимо выполнить  n – 1 умножений. Алгоритм быстрого (бинарного) возведения в степень позволяет сократить количество умножений. Алгоритм (для  n ≥ 0) может быть описан с помощью следующего рекуррентного соотношения:

a to the power of n space equals space open curly brackets table attributes columnalign left end attributes row cell 1 comma space a space equals space 0 end cell row cell open parentheses a to the power of n over 2 end exponent close parentheses squared comma space space space a space space long dash space четное end cell row cell a space space asterisk times space a to the power of n space – space 1 end exponent comma space space n space long dash space нечетное end cell end table close

Написать программу, которая находит an, используя описанный выше алгоритм.

Этапы выполнения задания

I. Исходные данные: числа a и n.

II. Результат: значение an.

III. Алгоритм решения задачи.

1. Ввод исходных данных.
2. Опишем рекурсивную функцию binpow(x, k).

2.1. Параметр x — основание степени, параметр k — показатель.
2.2. Условие прекращения рекурсии — параметр k равен 0. Функция должна вернуть 1. 
2.3. Если k — четное, то при вызове функции второй параметр уменьшаем в 2 раза, если нечетное, то второй параметр уменьшаем на 1.

3. Вывод результата.

IV. Описание переменных: a, n — int.

В некоторых декларативных или чисто функциональных языках (таких как Пролог или Haskell) нет синтаксических средств для организации циклов. В них рекурсия является единственным доступным механизмом для организации повторяющихся вычислений.

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

Классическим примером бесконечной рекурсии являются два поставленные друг напротив друга зеркала: в них образуются два коридора из уменьшающихся отражений зеркал.

Рекурсия лежит в основе построения фрактальных изображений.

Пример 7.14.

V. Программа:

#include <iostream>

 

using namespace std;

 

int gcd (int x, int y)

{

  if (== y)

    return x;

  else

    if (> y)

      gcd(- y, y);

    else

      gcd(x, y - x);

}

int main()

{

  int a, b;

  cout << "vvedi 2 chisla" << endl;

  cin >> a >> b;

  int d = gcd(a, b);

  cout << "NOD = " << d << endl;

  return 0;

}

 VI.  Тестирование:

Рекурсивная функция для вычисления НОД двух чисел может быть записана с использованием тернарной (от лат. ternarius — тройной) условной операции.

int gcd (int a, int b)

{

  return b ? gcd (b, a % b) : a;

}

Операция, структурно записываемая как o1 : o2 : o3, возвращает свой второй (o2) или третий (o3) операнд в зависимости от значения логического выражения, заданного первым операндом (o1).

Пример 7.15.

V. Программа:

#include <iostream>

 

using namespace std;

 

void razlog (int a, int b)

{

  if (> 1)

    if ( a % b == 0){

      razlog(/ b, b);

      cout << b << " ";

    }

    else

      razlog(a, b + 1);

}

int main()

{

  int n;

  cout << "n = ";

  cin >> n;

  razlog(n, 2);

  return 0;

}

 

VI. Тестирование:

VII. Анализ результата.

Чтобы понять, почему мы перебираем простые множители, начиная с меньшего, а программа выводит их в обратном порядке, распишем порядок выполнения команд в программе.

a

b

Комментарии

1

60

2

a > 1, 60 % 2 = 0, вызов (60 / 2, 2), после вызова есть команда вывода

2

30

2

a > 1, 30 % 2 = 0, вызов (30 / 2, 2), после вызова есть команда вывода

3

15

2

a > 1, 15 % 2 ≠ 0, вызов (15, 3) после вызова нет команд

4

15

3

a > 1, 15 % 3 = 0, вызов (15 / 3, 3), после вызова есть команда вывода

5

5

3

> 1, 5 % 3 ≠ 0, вызов (5, 4), после вызова нет команд

6

5

4

a > 1, 5 % 4 ≠ 0, вызов (5, 5), после вызова нет команд

7

5

5

a > 1, 5 % 5 = 0, вызов (5 / 5, 5), после вызова есть команда вывода

8

1

5

условие a > 1 не выполняется; при этом вызове функция закончила работу, однако не завершена работа при тех вызовах, после которых есть вывод.

9

5

5

вывод b = 5

10

15

3

вывод b = 3

11

30

2

вывод b = 2

12

60

2

вывод b = 2

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

Пример 7.16.

V. Программа:

#include <iostream>

 

using namespace std;

 

int binpow(int x, int k)

{

  if (== 0)

    return 1;

  if (% 2 == 1)

    return binpow(x, k-1) * x;

  else {

    int b = binpow(x, k / 2);

    return b * b;

  }

}

 

int main()

{

  int a, n;

  cout << "a = ";

  cin >> a;

  cout << "n = ";

  cin >> n;

  cout << a << "^" << n << " = ";

  cout << binpow(a, n) << endl;

  return 0;

}

 VI. Тестирование:



[1] С понятием стека вы познакомитесь позже.