Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Лк05 Рекурсивные алгоритмы.doc
Скачиваний:
30
Добавлен:
28.10.2018
Размер:
261.12 Кб
Скачать

21

Рекурсивные алгоритмы

  1. Понятие рекурсивных алгоритмов (РА). Применение РА при решении задач.

  2. Возможности получения рекурсии.

  3. Формы рекурсивных процедур.

  4. К вопросу о конце света.

  5. Сравнение рекурсивных и итеративных алгоритмов.

  6. Связь с математической индукцией.

  7. Анализ сложности РА.

  8. Понятие вычислительной сложности алгоритмов (по времени и памяти).

  9. Асимптотические верхние и средние оценки для итерационных и рекурсивных алгоритмов.

1. Понятие рекурсии

Еще одним подходом к проблеме формализации понятия алгоритма являются так называемые рекурсивные отношения.

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

Рекурсия кроется в идее "картины в картине" (вспомните рекламу о диванах и пьяном дяде), а также рисующих друг друга рук. Она обнаруживается в стихах Рингельнаца: "…а этот глист страдал глистами, что мучились глистами сами".

Вспомните и другие примеры рекурсии, знакомые вам (отражения в зеркалах, стихотворение "У попа была собака", наша Галактика, морские раковины и т.п.).

Хорошей моделью, на наш взгляд, служит известный стишок "Дом который построил Джек", а еще лучшей - пародия на него команды КВН Физтеха (1967).

Вот стенд, который построил студент,

А вот космическая частица,

Которая с бешеной скоростью мчится

В стенде, который построил студент.

А вот инженер молодой, бледнолицый,

Который клянет и судьбу и частицу,

Которая с бешеной скоростью мчится

В стенде, который построил студент.

А вот кандидат, горделивый не в меру,

Который блистательно сделал карьеру,

Совместно работая с тем инженером,

Который клянет и судьбу и частицу,

Которая с бешеной скоростью мчится

В стенде, который построил студент.

А вот начальник, на вид простоватый,

Который был шефом того кандидата,

Который блистательно сделал карьеру,

Совместно работая с тем инженером,

Который клянет и судьбу и частицу,

Которая с бешеной скоростью мчится

В стенде, который построил студент.

А вот консультант от академии,

С которым встречался время от времени

Тот самый начальник, на вид простоватый,

Который был шефом того кандидата,

Который блистательно сделал карьеру,

Совместно работая с тем инженером,

Который клянет и судьбу и частицу,

Которая с бешеной скоростью мчится

В стенде, который построил студент.

А вот отчетов горы бумажные,

В которых копалась комиссия важная,

Которая выдала крупную премию,

Тому консультанту из академии,

Начальнику дали за вид простоватый,

Кусок уделили тому кандидату,

Который блистательно сделал карьеру,

Остатки вручили тому инженеру,

Который уже не ругает частицу,

Которая с бешеной скоростью мчится

В стенде, который построил СТУДЕНТ.

Сначала поясним, что такое рекурсия.

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

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

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

По сути это означает, что некоторую подзадачу свели к точно такой же, но с другими исходными данными. Такое сведение является одной из базисных схем обработки информации и также называется рекурсией.

Теперь, надеемся, всем ясно, что такое рекурсия, позволяющая писать компактные программы. (Представьте, как сократится стишок, если вместо повторов вызывать предыдущие строки с последующим углублением.)

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

Несмотря на все недостатки, присущие рекурсии, она мне нравится. Как говорится, любят ни за что, не любят за всё.

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

Приведем примеры рекурсии.

Если где хоть как-то объясняется слово рекурсия, в качестве примера обязательно приводится программа вычисления факториала.

Пример 1. Алгоритм вычисления факториала неотрицательного натурального числа.

Функцию n! = 1·2·3·…·n можно легко вычислить с помощью итеративных конструкций, например, через for.

i := 1;

for i := 1 to n do f := f*i;

Однако существует еще одно определение факториала:

Здесь идет определение через саму функцию факториала, т.е. рекурсивное определение.

Использование рекурсии позволяет легко (почти дословно) запрограммировать вычисления по рекурсивным формулам.

Program factorial;

Var n: integer;

Function fact (i: integer): longint;

begin

If i=1 then fact := 1

else fact := i*fact(i-1)

end;

Begin

write ('n=');

readln(n);

writeln ('факториал = ',fact(n));

End.

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

Аналогично, с помощью конечного рекурсивного алгоритма можно определить бесконечное вычисление, причем алгоритм не будет содержать повторений фрагментов текста.

Пример 2. Алгоритм вычисления чисел Фибоначчи (каждое последующее число равно сумме двух предыдущих).

Если для факториала первое (итеративное) определение может оказаться проще, то для чисел Фибоначчи рекурсивное определение (именно его все и помнят!!!) выглядит значительно лучше, чем прямая формула.

Рекурсивное нахождение:

f(1)=1; f(2)=2;

для любого n>2 f(n)=f(n-1)+f(n-2);

Итеративное определение:

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

Program m;{Числа Фибоначчи}

Var i: Integer;

Function Fib(t: Integer): Integer;

begin

 if (t=1) or (t=2) then Fib:=1 else Fib:= Fib(t-1) + Fib(t-2);

end;{Fib}

Begin

   For i := 1 to 10 do Writeln(i, ' ', Fib(i));

End.

Проследим, например, как вычисляется пятый член ряда Фибоначчи. Для него значение функции должно равняться сумме четвертого и третьего членов. При вычислении четвертого члена функция обратиться к третьему и второму, при вычислении третьего - ко второму и первому, которые равны единице. Подобное "погружение" произойдет и при вычислении третьего члена как слагаемого четвертого.

То есть мы видим, что такое применение рекурсии на редкость неэффективно: мы заставляем программу совершать множество "движений", количество которых лавинообразно растет с ростом номером числа в ряду. Вся красота и изящность компактного написания программы свелась на нет, поскольку пошла во вред производительности. А как же надо было составить программу, чтобы работала была эффективной? Задать массив и заполнять его такой же функцией, но без рекурсии, обращаясь к уже подсчитанным членам ряда, помещенным в массив. Программа будет работать "мгновенно", но окажется не такой красивой. Как говорит Жванецкий, процесс важнее результата...

Пример 3. Программа позволит печатать полный текст стихотворения "У попа была собака".

Для тех, кто не знает, привожу полный текст стихотворения:

У попа была собака

Он ее любил

Она съела кусок мяса

Он ее убил и на камне написал:

А дальше по кругу то же самое.

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

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

program pop;

uses crt;

procedure absaz(c:word);{ Процедура,печатающая один абзац }

{ В качестве параметра выступает цвет абзаца }

begin

textcolor(c);

writeln(' У попа была собака');

writeln(' Он ее любил');

writeln(' Она съела кусок мяса');

writeln(' Он ее убил');

writeln(' И на камне написал:');

delay(500);{ Задержка перед выводом следующего абзаца }

if not keypressed then absaz((c+1)mod 15+1);

{ Продолжать, пока не нажата клавиша }

end;

Begin

clrscr;

absaz(1);

readkey;

End.

Пример 3. Назовем эту программу условно "Квадраты". Но если угодно, это могут быть ромбы, треугольники и другие фигуры. Как захотите, так и сделаете.

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

program kvadr;

uses crt,graph;

const n=10;

var l: word;

procedure new(x1,y1,x2,y2:integer;var xl,yl:integer);

{ Вычисление новой координаты вершины }

begin

xl:=(x1+(n-1)*x2) div n;

yl:=(y1+(n-1)*y2) div n;

end;

procedure postroit(x1,y1,x2,y2,x3,y3,x4,y4:integer);

{ Процедура построения четырехугольника }

var l1,k1,l2,k2,l3,k3,l4,k4:integer;

label qv;

begin

setcolor(l);

line(x1,y1,x2,y2);

line(x2,y2,x3,y3);

line(x3,y3,x4,y4);

line(x4,y4,x1,y1);

if abs(x2-x1)<0.001 then goto qv; { Проверка окончания построения }

new(x1,y1,x2,y2,l1,k1);

new(x2,y2,x3,y3,l2,k2);

new(x3,y3,x4,y4,l3,k3);

new(x4,y4,x1,y1,l4,k4);

delay(70);

postroit(l1,k1,l2,k2,l3,k3,l4,k4);

qv:

end;

var k,dr,md:integer;

x1,x2,x3,x4,y1,y2,y3,y4:integer;

Begin

l:=10;

x1:=120;y1:=40;

x2:=520;y2:=40;

x3:=520;y3:=440;

x4:=120;y4:=440;

dr:=9;md:=2;

initgraph(dr,md,'c:\bp\bgi');

setbkcolor(1);

postroit(x1,y1,x2,y2,x3,y3,x4,y4);

readln;

closegraph;

End.

Пример 4. Рассмотрим программу "Разбиение доски", которая позволит произвести подсчет частей, на которые развалится доска. При помощи рекурсии решается просто.

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

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

for i:=1 to m do for j:=1 to n do

begin

if doska.d[i,j]=1 then

begin

s:=s+1;

proverka(i,j);

end;

end;

Если находится клетка содержание которой равно единице, то запускается процедура proverka(i,j) главная цель которой - выявить кусок доски которому принадлежит клетка, заполнить, его значением s>1 (для каждого куска свое). Все это происходит по рекурсии следующим образом:

procedure traschet.proverka(a,b:byte);

begin

doska.d[a,b]:=s;

if doska.d[a-1,b]=1 then proverka(a-1,b);

if doska.d[a+1,b]=1 then proverka(a+1,b);

if doska.d[a,b-1]=1 then proverka(a,b-1);

if doska.d[a,b+1]=1 then proverka(a,b+1);

end;

Для красоты сделал некоторое подобие интерфейса (чтоб было проще доску на части раскалывать).

Работа программы строится на трех объектах: доски - tdoska, расчета - traschet и курсора - tkyrsor.

Архив программы находится здесь: RAR-архив.

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

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

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

Таким образом, какой-либо переменной a на различных уровнях рекурсии будет соответствовать различная ячейка памяти, которая может иметь различные значения. Поэтому для ввода следующих понятий воспользуемся значением переменной a i-го уровня (рекурсия может находиться на этом i-том уровне).

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

Число рекурсивных вызовов в каждый конкретный момент времени называется текущим уровнем рекурсии.

Пример 5. Чем отличаются программы L1 и L2, что у них общего?

Program L1;

procedure pr1;

begin

writeln ('Привет всем');

writeln ('Как живете?');

pr1;

end;

Begin

pr1;

End.

Program L2;

procedure pr2;

begin

pr2;

writeln ('Привет всем');

writeln ('Как живете?');

end;

Begin

Pr2;

End.

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