Лекция 8 1. Рекурсия

Реклама
Лекция 8
1. Рекурсия
Рекурсия (от латинского recursio – возвращение) — это такой способ
организации вспомогательного алгоритма (подпрограммы), при котором эта
подпрограмма (процедура или функция) в ходе выполнения ее операторов обращается
сама к себе. Вообще, рекурсивным называется любой объект, который частично
определяется через себя.
Пример 1. Что будет делать следующая программа?
#include <iostream>;
using namespace std;
void privet();
{
cout<<”Привет”;
privet();
}
void main()
{
privet();
}
Ответ: процедура будет бесконечно выводить на экран слово “Привет”.
В действительности, такой бесконечный вызов приводит к переполнению памяти и
возникновению ошибки.
Вообще, в рекурсивном определении должно присутствовать ограничение,
граничное условие, при выходе на которое дальнейшая инициация рекурсивных
обращений прекращается.
Примеры рекурсивных определений:
1. Определение факториала. С одной стороны, факториал определяется так:
n!=1*2*3*...*n.
С другой стороны,
Граничным условием в данном
случае является n<=1.
2. Определим функцию K(n), которая возвращает количество цифр в заданном
натуральном числе n:
Задание. По аналогии определите функцию S(n), вычисляющую сумму цифр
заданного натурального числа.
Обращение к рекурсивной подпрограмме ничем не отличается от вызова любой
другой подпрограммы. При каждом рекурсивном вызове информация о нем сохраняется в
специальной области памяти, называемой стеком. В стеке записываются значения
локальных переменных, параметров подпрограммы и адрес точки возврата. Таким
образом, какой-либо локальной переменной A на разных уровнях рекурсии будут
соответствовать разные ячейки памяти, которые могут иметь разные значения. Поэтому
воспользоваться значением переменной A i-ого уровня можно только находясь на этом
уровне. Информация в стеке для каждого вызова подпрограммы будет порождаться до
выхода на граничное условие. Очевидно, в случае отсутствия граничного условия,
неограниченный рост количества информации в стеке приведёт к аварийному завершению
программы за счёт переполнения стека.
Выполнение действий в рекурсивной подпрограмме может быть организовано одним
из вариантов. Порождение все новых вызовов рекурсивной подпрограммы до выхода на
граничное условие называется рекурсивным спуском. Максимальное количество вызовов
рекурсивной подпрограммы, которое одновременно может находиться в памяти
компьютера, называется глубиной рекурсии. Завершение работы рекурсивных
подпрограмм, вплоть до самой первой, инициировавшей рекурсивные вызовы, называется
рекурсивным возвратом.
1) Выполнение действий на рекурсивном спуске.
тип rec(параметры)
{
<действия на входе в рекурсию>;
If <проверка условия> rec(параметры);
[else S;]
}
Обычно проверяют, что с каждым рекурсивным вызовом значение какого-то
параметра уменьшается, и это не может продолжаться бесконечно.
#include <iostream>
#include <iomanip>
using namespace std;
int s=1;
int fact(int k)
{
if (k>=1)
{
s=s*k;
fact(k-1);
}
return s;
}
void main()
{
int n;
cin>>n;
s=fact(n);
cout<<s;
}
Уровень
0
1
2
3
4
5
Спуск
K
4
4
3
2
1
0
Возврат
S
1
4
12
24
24
Fact
Fact(4)
Fact(3)
Fact(2)
Fact(1)
Fact(0)
2) Выполнение действий на рекурсивном возврате.
тип Rec(параметры);
{
If <проверка условия> Rec(параметры);
[else S1];
<действия на выходе из рекурсии>;
}
#include <iostream>
#include <iomanip>
24
24
24
24
24
using namespace std;
int fact(int k)
{
if (k>=1) return (fact(k-1)*k);
else return (1);
}
void main()
{
int n;
cin>>n;
cout<<fact(n);
}
Уровень
0
1
2
3
4
5
Спуск
K
4
4
3
2
1
0
Возврат
Fact
Fact(4)
Fact(3)*4
Fact(2)*3
Fact(1)*2
Fact(0)*2
1
24
24
12
4
2
3) Выполнение действий на рекурсивном спуске и на рекурсивном возврате.
тип Rec (параметры);
{
<действия на входе в рекурсию>;
If <условие> Rec(параметры);
<действия на выходе из рекурсии>
}
или
тип Rec(параметры);
{
If <условие>
{
<действия на входе в рекурсию>;
Rec;
<действия на выходе из рекурсии>
}
}
Задание 1. Написать рекурсивную функцию нахождения n-го числа Фибонначи.
#include <iostream>
using namespace std;
int Fib(int n);
int main()
{
int n ;
cin >> n;
int y = Fib(n);
cout << "Fib(" << n << ")=" << y<< endl;
return 0;
}
int Fib(int n)
{
if(n <= 2)
return 1;
else
return Fib(n-1) + Fib(n-2);
}
Задание 2. Написать рекурсивную функцию нахождения цифр числа.
#include <iostream>
using namespace std;
int cifra(int n)
{
int s;
if (n==0) s=0;
else s=cifra(n/10)+n%10;
return s;
}
int main()
{
int n ;
cin >> n;
int y = cifra(n);
cout << "Sum="<< y<< endl;
return 0;
}
В языке C++ допускается также и косвенная рекурсия, когда, например, процедура,
процедура А вызывает процедуру В, а та, в свою очередь,- процедуру А. Длина таких
цепочек вызовов может быть произвольной, однако при разработке программы
необходимо тщательно следить за тем, чтобы рекурсивный алгоритм был сходимым, то
есть не приводил к бесконечным взаимным вызовам подпрограмм.
Пример
Программа иллюстрирует косвенные рекурсивные вызовы процедур. В этой
программе процедуры Rec1 и Rec2 рекурсивно вызывают друг друга, поочередно
уменьшая свои фактические параметры. Обе процедуры работают с одной глобальной
переменной А, которая передается в них по ссылке. Критерием завершения работы
является обращение этой переменной в ноль.
В программе необходимо предварительное определение прототипа второй
процедуры Rec2, так как ее вызов встречается в процедуре Rec1, т.е. перед ее полным
описанием.
#include <iostream>
using namespace std;
int A;
void Rec2 (int& Y);
void Rec1 (int& X)
{
X= X-1;
if (X>0)
{
cout<<X<<"\n";
Rec2(X);
}
}
void Rec2 (int& Y)
{
Y= Y /2;
if (Y>2)
{
cout<<Y<<"\n";
Rec1(Y);
}
}
void main()
{
A= 15;
Rec1(A);
cout<<A<<"\n";
}
Что будет напечатано на экране?
Ответ:
X=14
Y=7
X=6
Y=3
X=2
A=1
Использование рекурсии является красивым приёмом программирования.
Рекурсивные версии программ, как правило, гораздо короче и нагляднее. В то же время в
большинстве практических задач этот приём неэффективен с точки зрения расходования
таких ресурсов ЭВМ, как память и время исполнения программы. Использование
рекурсии увеличивает время исполнения программы и зачастую требует значительного
объёма памяти для хранения копий подпрограммы на рекурсивном спуске. Поэтому на
практике разумно заменять рекурсивные алгоритмы на итеративные. А любой
рекурсивный алгоритм можно преобразовать в эквивалентный итеративный (то есть
использующий циклические конструкции).
Ханойские башни – это классическая задача на использование рекурсии.
A
B
C
На площадке A находится пирамида из дисков разного размера. Требуется перенести
её на площадку B в том же виде, соблюдая следующие правила:
- перекладывать можно только по одному диску, взятому сверху пирамиды,
- класть диск можно только на основание площадки, либо на диск большего размера,
- площадка C является вспомогательной
Запишем алгоритм переноса:
1. Если N=0, то ничего не делать.
2. Если N>0, то
- перенести N-1 диск на C через B,
- диск A перенести на B через С,
- переместить N-1 диск на B через A.
При выполнении пункта 2 будет три состояния пирамид:
A
A
A
B
C
B
C
B
C
Построим программу на C++ с использованием рекурсивной процедуры Hanoi и
фактических имён площадок 1,2,3.
include <iostream>
using namespace std;
void Hanoy(int n, char A, char B, char C)
{
if (n>0)
{
Hanoy(n-1,A,C,B);
cout<<A<<"=>"<<B<<"\n";
Hanoy(n-1,C,B,A);
}
}
void main()
{
int N;
cout<<"Число дисков";
cin>>N;
Hanoy(N,'1','2','3');
}
Пусть N=3
Сбор информации по рекурсивному дереву идёт слева направо и сверху вниз. Таким
образом, получаем следующий алгоритм переноса:
1->2, 1->3, 2->3, 1->2, 3->1, 3->2, 1->2.
Скачать
Учебные коллекции