Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

C _Учебник_МОНУ

.pdf
Скачиваний:
206
Добавлен:
12.05.2015
Размер:
11.12 Mб
Скачать

Функції

289

Якщо розглядати цю послідовність, розпочинаючи від молодших членів до старших, спосіб її побудови задається циклічним алгоритмом, а якщо, навпаки, – від заданого n=n0, то спосіб визначення цього елемента через попередні буде рекурсивним.

Вочевидь, що рекурсія не може бути безумовною, бо у такому разі вона стає нескінченною. Про це свідчить принаймні наведена вище лічилка. Рекур-

сія повинна мати всередині себе умову завершення, за якою наступний крок рекурсії вже не виконуватиметься.

Рекурсивні функції лише на перший погляд схожі на звичайні фрагменти програм. Щоб відчути специфіку рекурсивної функції, варто простежити за текстом програми перебіг її виконування. У звичайній програмі будемо йти ланцюжком викликів функцій, але жодного разу повторно не увійдемо до того самого фрагменту, допоки з нього не вийшли. Можна стверджувати, що перебіг виконування програми “лягає” однозначно на текст програми. Інша річ – рекурсія. Якщо спробувати відстежити за текстом програми перебіг її виконування, то дістанемось такої ситуації: увійшовши до рекурсивної функції, “рухаємося” її текстом доти, допоки не зустрінемо її виклик (можливо, з іншими параметрами), після чого знову розпочнемо виконувати ту ж саму функцію спочатку. При цьому слід зазначити найважливішу властивість рекурсивної функції: її перший виклик ще не завершився. Чисто зовні складається враження, що текст функції відтворюється (копіюється) щоразу, коли функція сама себе викликає. Насправді цей ефект відтворюється в комп‟ютері. Однак копіюється при цьому не весь текст функції (не вся функція), а лише її частини, пов‟язані з локальними даними (формальні, фактичні параметри, локальні змінні й точка повертання). Алгоритмічна частина (оператори, вирази) рекурсивної функції й глобальні змінні не змінюються, тому вони присутні в пам‟яті комп‟ютера в єдиному екземплярі.

Кожний рекурсивний виклик породжує новий “екземпляр” формальних параметрів і локальних змінних, причому старий “екземпляр” не знищується, а зберігається у стеку на засаді вкладеності як “матрьошки”. Тут має місце єдиний випадок, коли одному імені змінної в перебігу роботи програми відповідають кілька її екземплярів. Відбувається це в такій послідовності:

у стеку резервується місце для формальних параметрів, куди записуються значення фактичних параметрів. Зазвичай це виконується в порядку, зворотному до їхнього місця у списку;

при виклику функції до стека записується точка повертання – адреса тієї частини програми, де міститься виклик функції;

на початку тіла функції у стеку резервується місце для локальних (автоматичних) змінних.

Зазначені змінні створюють групу (фрейм стека). Стек “пам‟ятає історію” рекурсивних викликів у вигляді послідовності (ланцюжка) таких фреймів. Програма у кожний конкретний момент працює з останнім викликом і з останнім фреймом. По завершенні рекурсії програма повертається до попередньої версії рекурсивної функції й до попереднього фрейму у стеку.

Послідовність рекурсивних викликів можна прокоментувати приблизно

такою фразою: “Функція F виконує ... і викликає F, яка виконує ... і викликає F...”.

290

Розділ 8

Класичним прикладом рекурсивної функції є обчислення факторіала (це не означає, що факторіал треба обчислювати лише в такий спосіб). Для того щоб визначити факторіал числа n, слід помножити n на факторіал числа (n-1):

n! = n*(n-1)! // Це так зване рекурентне співвідношення.

Відомо також, що 0!=1 й 1!=1, тобто умовою зупинки рекурсії буде: якщо n=0 чи n=1, то факторіал дорівнює 1.

Дамо повне рекурсивне визначення факторіала:

n 1 ! n,

n 0;

n!

n 0.

1,

Отже, рекурсивна функція обчислення факторіала числа n виглядатиме так:

long fact (long n)

{ if (n==0 || n==1) return 1; return (n*fact(n-1)); }

Якщо у функції main() зустрінеться виклик функції fact(3), при виконуванні цієї функції буде викликано функцію fact(2), яка своєю чергою викличе fact(1). Для останньої спрацює умова зупинки рекурсії (n==1) і у функцію fact(2) буде повернуто значення 1 та обчислено значення 2 (2!= 2), яке своєю чергою буде повернуто до функції fact(3)і буде використано для обчислення 3!.

Отже, програма має 3 рекурсивні виклики. Занурившись на три рівні рекурсії, програма дісталася “глухого кута” – граничної точки, після чого вона зворотним ланцюжком має піднятися на ті самі три рівні. В результаті кожного рекурсивного виклику функція зберігає своє значення змінної n, оскільки до третього виклику у неї буде вже три окремих змінних з ім‟ям n, при цьому кожна має власне, відмінне від інших, значення, однак тіло функції буде присутнім в пам‟яті в єдиному екземплярі. Цей ланцюжок рекурсивних викликів можна зобразити в такий спосіб:

Породження усіх нових копій рекурсивної функції до виходу на граничну умову називається рекурсивним спуском. Максимальна кількість копій рекурсивної функції, яке водночас може міститися у пам‟яті комп‟ютера, називається глибиною рекурсії. У разі відсутності граничної умови необмежене зростання кількості таких копій призведе до аварійного завершення програми за рахунок переповнення стека. Завершення роботи рекурсивних функцій, аж до самої першої, яка ініціювала рекурсивні виклики, називається рекурсивним підйомом.

Функції

291

При створюванні рекурсивної функції завжди слід добре поміркувати, чи ефективно є застосовувати рекурсивний алгоритм, оскільки рекурсія з надто великою кількістю вкладених викликів може вичерпати ресурси оперативної пам‟яті, що спричинить збій у роботі операційної системи.

Розглянемо алгоритм нераціонального використання рекурсії на прикладі обчислення чисел Фібоначчі – числового ряду, в якому кожен наступний член є сумою двох попередніх: 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144 і т.д. Італійський математик середньовіччя Леонардо Фібоначчі відкрив цю числову послідовність, вивчаючи піраміду Хеопса у Гізі. До речі, ці числа пов‟язані поміж собою цікавими співвідношеннями. Наприклад, кожне число є приблизно в 1.618 разів більше попереднього (цю пропорцію називають золотою), у 2.618 рази більше того, яке розташоване через одне число від нього і в 4.236 разів більше числа, розміщеного двома числами раніш, а кожне попереднє число складає приблизно 0.618 від наступного. Поширеність співвідношень Фібоначчі у житті просто вражає. Принцип золотого перетину – найвищий прояв структурної й функціональної довершеності цілого та його частин у мистецтві, науці, техніці й природі. Закономірності “золотої” симетрії проявляються в енергетичних переходах елементарних часток, у будові деяких хімічних сполук, у планетарних і космічних системах, у генних структурах живих організмів, у принципах формотворення рослин тощо. Ці закономірності, виявлено як у будові окремих органів людини, так і тіла у цілому, а також вони проявляються у біоритмах і функціонуванні головного мозку і зорового сприйняття.

Ітераційна функція обчислення чисел Фібоначчі має вигляд

long fib1(int n)

{ long a=0, b=1, c=n; for(int i=2; i<=n; i++)

{ c=a+b; a=b; b=c; } return c;

}

Рекурсивна функція матиме вигляд long fib2(int n)

{if(n==0 || n==1) return n;

else return fib2(n-1)+fib2(n-2); }

або

long fib3(int n)

{ return (n>1) ? fib3(n-1)+fib3(n-2) : n; }

Така рекурсивна функція є придатна для використання лише для невеликих значень n, оскільки кожне звертання призводить до двох подальших викликів, причому викликів з одним і тим самим значенням параметра, тобто кількість викликів зростає експоненціально. Отже, у цьому завданні доцільніше застосовувати ітераційну функцію fib1.

Рекурсивні функції найчастіше застосовують для компактної реалізації рекурсивних алгоритмів, а також для роботи зі структурами даних, описаними рекурсивно, наприклад з бінарними деревами. Кожну рекурсивну функцію можна реалізувати без застосування рекурсії. Достоїнством рекурсії є компактний

292

Розділ 8

запис, а недоліками – витрата часу й пам‟яті на повторні виклики функції й передавання їй копій параметрів і, головне, небезпека переповнення стека.

Приклад 8.13 Увести ціле число і вивести всі цифри, які зображують це число.

Текст консольної програми:

void cnum (int n) { int a=10;

іf (n==0) return;

else { cnum(n/a); cout << n%a<<endl; }

}

void main() { int n;

cout << "Увести ціле число: "; cin >> n; cnum(n);

getch(); return 0;

}

Результати виконання програми:

Увести ціле число: 1234 1 2 3 4

Приклад 8.14 Створити рекурсивну функцію піднесення числа до степеня. Аргументами цієї функції мають бути певне дійсне число, яке буде підноситись до степеня, і, безпосередньо, показник степеня цілого типу.

Тексти функції та її виклику в основній пр ограмі:

#include <iostream.h> #include <conio.h>

double power(double x, int st)

{if(st<=0) return 1;

else return(x*power(x,st-1));

}

void main()

{double x; int st;

cout<<"Увести число x= "; cin>>x; cout<<"Увести показник степеня st= "; cin>>st; cout<<"x ^ st= " << power(x, st);

getch(); return 0;

}

Результати виконання програми:

Увести число x= 2.5

Увести показник степеня st= 2 x ^ st= 6.25

Функції

293

Приклад 8.15 Написати рекурсивну функцію, яка обчислює мінімальний елемент масиву.

Розв‟язок. Створимо рекурсивну функцію minimum(), яка повертатиме індекс мінімального елемента. Вона матиме три параметри: 1) вказівник на початок масиву mas, 2) розмір масиву n і 3) індекс елемента, з якого розпочинається підмасив. Цей підмасив рекурсивно зменшується, допоки його розмір не становитиме 1. Коли у ньому залишиться один елемент (k==n-1), повертається значення k. На наступному кроці рекурсивного повертання порівнюються mas[k] (останній елемент) та mas[a] (передостанній елемент) та повертається індекс меншого з них. Продовжуючи, на кожному кроці порівнюються мінімум, здобутий на попередніх кроках, і mas[a] (поточний елемент).

Тексти функції та її виклику в основній пр ограмі: int minimum(int *mas, int n, int k)

{if(k == n-1) return k;

int a = minimum(mas,n,k+1);// Виклик для підмасиву розміром на 1 менше

if(mas[a]<mas[k]) return a; else return k;

}

 

void main()

 

{ int i, n, min;

 

cout<<"Уведіть розмір масиву: ";

cin>>n;

int *mas = new int[n];

 

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

 

{ cout<<"Уведіть mas["<<i<<"]: ";

cin>>mas[i]; }

cout<<endl;

 

min = minimum(mas,n,0);// Виклик для всього масиву (k=0). cout<<"Мінімум: mas["<<min<<"] = "<<mas[min]<<endl; getch();

return 0;

}

Приклад 8.16 Написати рекурсивну функцію, яка обчислює суму квадратних коренів натуральних чисел від 1 до n.

Тексти функції та її виклику в основній пр ограмі:

#include <iostream.h> #include <conio.h> #include <math.h> double sumkor(int a)

{if (a==1) return 1;

else return (sqrt(a) + sumkor(a-1));

}

void main()

{int n;

cout<<"n= "; cin>>n; cout<<sumkor(n);

getch(); return 0;

}

294

Розділ 8

Приклад 8.17 Увести масив з 10-ти дійсних чисел і створити рекурсивну функцію, яка записує масив у зворотній послідовності .

Розв‟язок. Функція invertItem()змінює місцями елементи з індексами i та j. Спочатку це перший та останній елементи масиву, потім індекс i збільшується, а j – зменшується – і функція викликає себе для обміну другого й передостаннього елементів. Ланцюжок викликів триває, допоки i < j.

Тексти функції та її виклику в основній пр ограмі: void invertItem(double *a, int i, int j)

{double t=a[i]; a[i]=a[j]; a[j]=t; i++; j--;

if (i<j) invertItem(a, i, j);

}

void main()

{double a[10]; int i;

cout<<"Увести 10 дійсних чисел:"<<endl; for (i=0; i<10; i++) cin>>a[i]; invertItem(a, 0, 9);

for (i=0; i<10; i++) cout<<a[i]<<" "; getch(); return 0;

}

Результати виконання програми:

Увести 10 дійсних чисел:

0 1 2 3 4 5 6 7 8 9

9 8 7 6 5 4 3 2 1 0

Приклад 8.18 Визначити, чи є введений рядок паліндромом, тобто таким, що читається однаково зліва направо і справа наліво.

Тексти функції та її виклику в основній прог рамі:

int pal(char *s)

{int len; char *s1=new char[strlen(s)+1]; if (strlen(s)<=1) return 1;

else

{len=(s[0]==s[strlen(s)-1]); strncpy(s1, s+1, strlen(s)-2); s1[strlen(s)-2]='\0';

return len && pal(s1);

}

Функції

295

delete []s1;

}

void __fastcall TForm1::Button1Click(TObject *Sender)

{AnsiString ss = Edit1->Text; char *s = new char [ss.Length()]; strcpy(s,ss.c_str());

int p = pal(s);

if(p)ShowMessage("Рядок є паліндромом"); else ShowMessage("Рядок не є паліндромом");

}

Зверніть увагу на команду: l=(s[0]==s[strlen(s)-1]);

У ній спочатку виконується порівняння s[0]==s[strlen(s)-1], а потім його результат (true чи false) перетворюється на ціле число (відповідно 1 чи 0) і присвоюється змінній l.

Запис

return l && pal(s1);

означає, що після обчислення pal(s1) обчислюється результат операції логічного множення && і цей результат повертається у точку виклику функції.

Приклад 8.19 Увести два числа й обчислити найбільший спільний дільник (НСД) двох цілих чисел, застосовуючи рекурсивний алгоритм Евкліда.

Розв‟язок. Нерекурсивна реалізація алгоритму Евкліда обчислення НДС двох цілих чисел наведена у п. 4.4.4.

Тексти функції та основної пр ограми:

int nod(int a, int b) { int c;

// За умови b>a параметри змінюються місцями if(b > a) c = nod(b, a);

else

if(b == 0) c = a;

else c = nod(b, a%b); // Тут a%b – залишок від ділення a на b return c;

}

void __fastcall TForm1::Button1Click(TObject *Sender)

{int a, b;

a = StrToInt(Edit1->Text); b = StrToInt(Edit2->Text);

Edit3->Text = IntToStr(nod(a, b));

}

Приклад 8.20 Увести масив з 12-ти цілих чисел і упорядкувати їх за зростанням методом швидкого сортування за допомогою рекурсивної функції.

Розв‟язок. Швидке сортування (англ. quicksort), часто зване qsort на ім‟я реалізації в стандартній бібліотеці мови С – широко відомий алгоритм сорту-

296

Розділ 8

вання, розроблений англійським інформатиком Чарльзом Хоаром. Це один з швидких відомих універсальних алгоритмів сортування масивів.

Коротко описуючи цей алгоритм, треба обрати опорний елемент, порівняти решту елементів з опорним, на підставі порівняння розбити множину елементів масиву на три – “менші опорного”, “рівні” і “більші”, розташувати їх у порядку менші-рівні-більші, після чого повторити рекурсивно ці дії для “менших” і “більших”.

Зазвичай опорним елементом вибирають середній елемент, хоча з точки зору коректності алгоритму це не є суттєвим. Операція реорганізації масиву полягає в тому, щоб усі елементи, які є менші чи рівні опорному елементові, опинилися ліворуч від нього, а усі елементи, більші за опорний, – праворуч від нього. Для цього у функції QuickSort() організовано таку послідовність дій:

1)два індекси – L та R, прирівнюються до мінімального та максимального індексів розділеного масиву відповідно: i=L; j=R;

2)обчислюється індекс опорного елемента n=(L+R)/2;

3)індекс i послідовно збільшується допоки i елемент не перевищить опорний: while (a[i] < x) i++;

4)індекс j послідовно зменшується допоки j-й елемент не виявиться менше чи рівний опорному: while (x < a[j]) j--;

5)якщо i<j – знайдену пару елементів треба поміняти місцями й продовжити операцію поділу з тих значень i та j, які були досягнені;

6)чергуючи зменшення j та збільшення i, порівняння та необхідні обміни, віднаходимо середину масиву, коли i=j, тобто операцію поділу завершено, обидва індекси вказують на опорний елемент. У результаті елементи масиву виявляються розділеними на дві частини так, що всі елементи ліворуч є менші за опорний елемент, а всі елементи праворуч – більше за нього;

7)рекурсивно впорядковуємо підмасиви, що лежать ліворуч і праворуч від опорного елемента, застосовуючи розглянутий алгоритм. Оскільки на кожній ітерації (на кожному наступному рівні рекурсії) довжина оброблюваного відрізка масиву зменшується, щонайменше, на одиницю, термінальна гілка рекурсії буде досягнена завжди й опрацювання гарантовано завершиться.

Цікаво, що Хоар розробив цей метод для машинного перекладу: справа в тім, що в той час словник зберігався на магнітній стрічці, і якщо впорядкувати всі слова в тексті, їх переклади можна дістати за один прогін стрічки.

Тексти функції та її виклику в основній пр ограмі:

void QuickSort(int a[], int L, int R)

// L – ліва та R – права межі сортування

{ int i=L, j=R, x, y; int n=(L+R)/2;

x = a[n]; do

{while (a[i] < x) i++; while (x < a[j]) j--;

Функції

297

if ( i <= j )

{y=a[i]; a[i]=a[j]; a[j]=y; i++; j--;

}} while (i <= j);

if (L < j) QuickSort(a, L, j); if (i < R) QuickSort(a, i, R); return;

}

void __fastcall TForm1::Button1Click(TObject *Sender)

{int i, a[12];

for (i=0; i<12; i++) a[i]=StrToInt(Memo1->Lines->Strings[i]);

QuickSort(a, 0, 11 ); // На вході ліва і права межа сортування

Memo2->Clear();

for (i=0; i<12; i++) Memo2->Lines->Add(IntToStr(a[i]));

}

8.6 Перевантаження функцій

Часто буває зручно, щоб функції, які реалізовують однаковий алгоритм для різних типів даних, мали однакове ім‟я. Якщо це ім‟я несе певну інформацію, це робить програму більш зрозумілою, оскільки для кожної дії треба пам‟ятати лише одне ім‟я. Використовування кількох функцій з однаковим ім‟ям, але з різними типами параметрів називається перевантаженням функцій.

Компілятор визначає, яку саме функцію слід викликати, за типом фактичних параметрів. Цей процес називається дозволом перевантаження (переклад з англійського слова “resolution”). Тип значення, яке повертає функція, у дозволі не бере участі. Механізм дозволу ґрунтується на доволі складному наборі правил, зміст яких зводиться до того, щоб використати функцію з найбільш придатними аргументами й видати повідомлення, якщо така не віднайдеться. Мета перевантаження – зробити так, щоб функції з однаковим ім‟ям виконувалися по-різному (і за можливість повертали різні значення) при звертанні до них з різними за типами й кількістю параметрами. За приклад наведемо чотири варіанти функції для визначення максимального значення:

int max(int, int); // Повертає найбільше з двох цілих

char* max(char*, char*);//Повертає рядок максимальної довжини

298

Розділ 8

//Повертає найбільше значення з першого параметра і довжини рядка, переданого

//до функції в якості другого параметра

int max(int, char*);

//Повертає найбільше значення з першого параметра і довжини рядка, переданого

//до функції в якості першого параметра

int max(char*, int);

void f(int a, int b, char* c, char* d)

{cout << max(a,b) << max(c,d) << max(a,c) << max(c,b); }

Усі чотири функції мають однакове ім‟я (max) й відрізняються типами параметрів (тому вважаються за різні функції). За викликання функції max() компілятор обирає відповідно до типу фактичних параметрів варіант функції (у наведеному прикладі буде послідовно викликано всі чотири варіанти функції). Якщо точної відповідності не віднайдено, виконуються перетворювання порядкових типів відповідно до загальних правил, наприклад bool й char до int, float до double тощо. Далі виконуються стандартні перетворювання типів, наприклад int до double, вказівників до void* тощо. Наступним кроком є виконання перетворювань типів, заданих користувачем, а також пошук відповідностей за рахунок змінної кількості аргументів функції. Якщо відповідність на тому самому етапі може бути набута у понад один спосіб, виклик вважається за неоднозначний і видається повідомлення про помилку.

Неоднозначність може виникнути за:

перетворення типу;

використання параметрів-посилань;

використання аргументів за замовчуванням. Приклад неоднозначності за перетворення типу:

#include <iostream.h> int f(int a){return a;}

int f(int a, int b=1){return a*b;} void main ()

{cout << f(10,2); // Викликається f(int, int)

cout << f(10); } // Неоднозначність – що викликати: f(int, int) чи f(int)?

Слід відрізняти перевантаження функцій і редекларацію. Якщо функції з однаковим ім‟ям мають однакові параметри і типи результату, вважається, що друге визначення розглядатиметься як редекларація (повторне визначення). Якщо параметри функцій з однаковим ім‟ям збігаються і функцій відрізняються лише типом значення, яке повертається, це призведе до помилки.

Правила опису перевантажених функцій:

перевантажені функції мають перебувати в одній області видимості, інакше станеться приховування аналогічне до однакових імен змінних у вкладених блоках;

перевантажені функції можуть мати параметри за замовчуванням, при цьому значення одного й того самого параметра у різних функціях мають збігатися. У різних варіантах перевантажених функцій може бути різна кількість параметрів за замовчуванням.

Соседние файлы в предмете [НЕСОРТИРОВАННОЕ]