• Nie Znaleziono Wyników

Podstawy algorytmiki i programowania - wykład 3 Funkcje rekurencyjne

N/A
N/A
Protected

Academic year: 2022

Share "Podstawy algorytmiki i programowania - wykład 3 Funkcje rekurencyjne"

Copied!
27
0
0

Pełen tekst

(1)

1

Funkcje rekurencyjne

Treści prezentowane w wykładzie zostały oparte o:

S. Prata, Język C++. Szkoła programowania. Wydanie VI, Helion, 2012

www.cplusplus.com

Jerzy Grębosz, Opus magnum C++11, Helion, 2017

B. Stroustrup, Język C++. Kompendium wiedzy. Wydanie IV, Helion, 2014

S. B. Lippman, J. Lajoie, Podstawy języka C++, WNT, Warszawa 2003.

Podstawy algorytmiki i programowania -

wykład 3

(2)

Funkcje rekurencyjne

Funkcje mogą wywoływać same siebie - takie funkcje nazywamy funkcjami rekurencyjnymi.

void f(int x){

f(x);

}

Przy definiowaniu takich funkcji trzeba zadbać, aby ciąg wywołań skończył się (aby nie doprowadzić do

nieskończonej pętli wywołań jak w powyższym przykładzie).

Zatem przed wywołaniem samej siebie funkcja zwykle

sprawdza pewien warunek i jeśli nie jest on spełniony, nie dokonuje już samo-wywołania.

Warunek ten nazywamy warunkiem zatrzymującym

(3)

3

Funkcje rekurencyjne void fun(int x){

if(x > 0) fun(x-1);

}

Funkcja ma warunek if(x > 0), dzięki któremu jej wywoływanie ma szanse się kiedyś zakończyć. Jeżeli wywołamy tę funkcję tak:

fun(3),

to wywoła ona siebie samą z argumentem 3 – 1 = 2.

Zacznie się więc ponowne wykonywanie funkcji: fun(2).

W niej nastąpi kolejne wywołanie, tym razem z argumentem 2 – 1 = 1, czyli

fun(1).

Teraz nastąpi kolejne wywołanie, tym razem z argumentem zero (bo:

1 – 1 = 0) fun(0).

To wywołanie funkcji sprawdzi warunek if(x>0) – i okaże się on niespełniony, więc dalsze wywołanie już nie nastąpi. Funkcja wykona się do końca, po czym powróci do poprzedniej, która też wykona się do końca, i tak dalej.

(4)

Obiekty definiowane w funkcji rekurencyjnej

Wiemy, że w zwykłych sytuacjach, jeżeli jedna funkcja A wywoła funkcję B, to lokalne zmienne funkcji A (przechowywane na stosie) nie giną.

Jeżeli ta funkcja B zdefiniuje jakieś swoje lokalne zmienne

(obiekty), także pojawiają się one na stosie. Po zakończeniu pracy funkcji B jej lokalne zmienne są usuwane ze stosu, a funkcja A

pracuje na tych swoich, które na nią czekały.

W przypadku wywołań rekurencyjnych jest podobnie. Gdy funkcja A wywołuje siebie samą (czyli drugi raz funkcję A), na stosie są już zmienne będące własnością pierwszego wywołania.

Drugie wywołanie spowoduje, że na stosie pojawią się nowe zmienne dla tego drugiego wywołania. Oczywiście będą to zupełnie odrębne zmienne.

(5)

5

Funkcja n!- wersja iteracyjna

unsigned long long silniaIt(unsigned short n){

//silnia: n!=1*2*...*n, 0!=1!=1 unsigned long long sil = 1;

for(int i=1; i<=n; i++) sil *= i;

return sil;

}

int main() {

cout << "5!=" << silniaIt(5)<<endl;

unsigned short n;

cout<<"podaj liczbe";

cin>>n;

cout<<n<<"!="<<silniaIt(n)<<endl;

}

(6)

Funkcje rekurencyjne - przykład n!

Przykład 1. Funkcja obliczająca n!, dla n>=0 //silnia: n!=(n-1)!*n, 0!=1!=1

unsigned long long silnia(unsigned short n) { if(n<=1)// warunek stopu:n==0 lub n==1 return 1;// 0! =1 1!=1

return n*silnia(n-1);

}

Dopóki zmienna n nie stanie się <=1, funkcja wywołuje się rekurencyjnie. Z analizy teoretycznej wynika, że po skończonej liczbie kroków wartość n musi stać się równa 0 lub 1. Tak więc warunek zakończenia (tzw. warunek stopu) jest zapewniony i funkcja działa prawidłowo.

(7)

7

Funkcje rekurencyjne - przykład n!

int main() {

cout << "5!=" << silnia(5)<<endl;

unsigned short n;

cout<<"podaj liczbe";

cin>>n;

cout<<n<<"!="<<silnia(n)<<endl;

}

(8)

Funkcje rekurencyjne - przykład n!

silnia(5)=5*silnia(4) =5*24 =120 wywołanie powrót

4*silnia(3) =4*6=24 wywołanie powrót

3*silnia(2) =3*2=6 wywołanie powrót

2*silnia(1) =2*1 wywołanie powrót 1

(9)

9

Funkcje rekurencyjne - przykład n!

(10)

Funkcje rekurencyjne - przykład nwd

Przykład. Rozpatrzmy funkcję nwd, która oblicza i zwraca

największy wspólny dzielnik swoich dwóch argumentów, które są liczbami naturalnymi (całkowitymi dodatnimi).

Algorytm Euklidesa nwd(a,0)=a,

nwd(a,b)=nwd(b,a mod b)

Przykładowo dzielenie 84 przez 18 daje iloraz równy 4 i resztę 12:

nwd(84,18)=nwd(18,12)

Podzielenie 18 przez 12 daje iloraz 1 i resztę równą 6:

nwd(18,12)=nwd(12,6)

Ostatecznie podzielenie 12 przez 6 daje zerową resztę:

nwd(12,6)=nwd(6,0)

co oznacza, że 6 jest największym wspólnym dzielnikiem 84 i 18.

(11)

11

Funkcje rekurencyjne - przykład nwd nwd(a,0) = a,

nwd(a,b) = nwd(b,a mod b)

unsigned int nwd(unsigned int a,unsigned int b) {

if(b == 0)

return a;

return nwd(b, a % b);

}

Dopóki zmienna b nie stanie się równa zeru, funkcja wywołuje się rekurencyjnie. Z analizy teoretycznej wynika, że dla

prawidłowych danych (a więc liczb dodatnich) po skończonej liczbie kroków wartość b musi stać się równa zeru. Tak więc warunek zakończenia (tzw. warunek stopu) jest zapewniony i funkcja działa prawidłowo.

(12)

Funkcje rekurencyjne - przykład nwd Krótszy zapis:

unsigned int nwd(unsigned int a,unsigned int b) { return b == 0 ? a : nwd(b, a % b);

}

int main() {

unsigned int x, y;

x = 84; y = 18;

cout << "nwd(" << x << "," << y << ")= "

<< nwd(x,y) << endl;

}

nwd(84,18) → nwd(18,12) → nwd(12,6) → nwd(6,0)=6

(13)

13

Funkcje rekurencyjne - przykład nwd

unsigned int nwd(unsigned int a,unsigned int b) { return b == 0 ? a : nwd(b, a % b);

}

„Samowywołanie” się funkcji jest w zasadzie normalnym wywołaniem: wszystkie lokalne zmienne są tworzone

oddzielnie w każdym wywołaniu, mechanizm przekazywania argumentów jest taki sam jak dla funkcji nierekursywnych.

Na przykład funkcja nwd tworzy zmienne lokalne a i b a

następnie, obliczając wyrażenie za dwukropkiem, wywołuje tę samą funkcję nwd i czeka na zwrócenie wyniku. To następne

„wcielenie” funkcji też tworzy swoje zmienne lokalne a i b, wywoła nwd i czeka na wynik, aby go zwrócić, itd. Kiedy w końcu b stanie się zero, funkcja zwróci a, które zostanie zwrócone przez poprzednie „wcielenie”, które zostanie zwrócone przez poprzednie „wcielenie”, itd.

(14)

Funkcje rekurencyjne - przykład nwd

Funkcje rekurencyjne trzeba stosować z umiarem i

umiejętnie. Często iteracyjne wersje funkcji są bardziej efektywne.

int nwdIt(unsigned int x, unsigned int y) { int temp;

while (y != 0) {

temp = y;

y = x % y;

x = temp;

}

return x;

(15)

15

Funkcje rekurencyjne - przykład: Ciąg Fibonacci’ego

Napisać funkcję obliczającą wartość n-tej liczby Fibonacci’ego danej wzorem:

F0=0, F1=1, Fn=Fn-1+ Fn-2, n>=2.

#include<iostream>

#include <ctime>

using namespace std;

int licznik; /*zmienna globalna służąca do zliczania ilości wywołań f-cji Fib */

unsigned long long Fib(unsigned int n) {

licznik++;

if( n < 2 ) return n;

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

}

(16)

Funkcje rekurencyjne - przykład: Ciąg Fibonacci’ego

Funkcje rekurencyjne trzeba stosować z umiarem i umiejętnie.

Nieprzemyślane zastosowanie rekurencji może bowiem prowadzić do kombinatorycznej eksplozji liczby wywołań i rozmiaru stosu potrzebnego do realizacji rekurencji.

Tak na przykład bywa, gdy w treści funkcji wywołanie samej siebie występuje dwukrotnie, jak np. w klasycznym

przykładzie obliczania wartości wyrazów ciągu Fibonacciego.

Wartość n-tej liczby Fibonacci’ego jest dana wzorem: F0=0, F1=1, Fn=Fn-1+ Fn-2, n>=2.

Rekurencja jest tu nieefektywna, bo zadanie o rozmiarze n

(17)

17

Funkcje rekurencyjne - przykład: Ciąg Fibonacci’ego

//wersja iteracyjna

unsigned long long FibIt(unsigned int n) {

if(n<=1) return n;

unsigned long long f, f0 = 0, f1 = 1;

//pozostałe elem.liczymy ze wzoru rekurenc.

for(int i = 2; i <= n; i++) {

f = f0 + f1;

f0 = f1;

f1 = f;

}

return f;

}

(18)

Funkcje rekurencyjne - przykład: Ciąg Fibonacci’ego int main()

{ cout<<"Liczby Fibonacciego (rekur) : f(40)"

<<endl;

licznik = 0;

clock_t start = clock();

cout << Fib(40) << endl;//102334155 cout << "Czas (s): " //1.198

<< ((float)(clock() - start))/CLOCKS_PER_SEC << "Licznik = " <<licznik; // 331160281

cout<<"\nwersja iteracyjna "<<endl;

start = clock();

cout << FibIt(40) << endl;

cout << "Czas (s): " //0.001

<< ((float)(clock() - start))/CLOCKS_PER_SEC;

(19)

19

Funkcje rekurencyjne - przykład: zapis binarny

Funkcja, która przy pomocy rekurencji wypisuje na ekranie daną liczbę w zapisie binarnym.

Algorytm :

1)Dzielimy liczbę przez 2: otrzymujemy rezultat dzielenia (całkowity) oraz resztę (0 lub 1). Zapamiętujemy tę resztę.

Natomiast rezultat całkowity zastępuje dotychczasową liczbę.

2)(Warunek zatrzymania rekurencji). Sprawdzamy, jaka jest (ta nowa) liczba.

· Jeśli jest równa 1, to przechodzimy do punktu 3 (STOP).

· Jeśli jest inna, to na niej powtórnie wykonujemy

czynności opisane w punkcie 1(wywołanie rekurencyjne).

3)Wypisujemy na ekranie owe reszty w odwrotnej kolejności od tej, jak je otrzymywaliśmy. Powstanie z nich wówczas na ekranie liczba w zapisie dwójkowym

(20)

Funkcje rekurencyjne - przykład: zapis binarny

(rys. [3])

(21)

21

Funkcje rekurencyjne - przykład: zapis binarny

void reprBin(int n) {

int reszta = n % 2;//reszta z dzielenia if(n > 1) // warunek zatrzymujący

{ //wywołanie rekurencyjne reprBin(n / 2);

}

cout << reszta;

/*Instrukcja cout wykonywana już po wywołaniu rekurencyjnym, a więc będzie ona wykonywana w trakcie powrotów.

Następuje tu wypisanie na ekranie zapamiętanej wcześniej reszty z dzielenia.*/

}

(22)

Funkcje rekurencyjne - przykład: zapis binarny

int main()

{ int liczba = 14;

cout << "\n" << liczba << " to dwojkowo ";

reprBin(liczba);//1110 }

W powyższym algorytmie skorzystaliśmy z faktu,że

rekurencja najpierw coraz bardziej się „zagnieżdża”, a gdy następuje warunek zatrzymania, zaczynają się powroty z tych „zagnieżdżeń”.

Dlatego chociaż w algorytmie Euklidesa poszczególne cyfry liczby dwójkowej otrzymujemy od cyfry najmniej znaczącej do najbardziej znaczącej( czyli od prawej do lewej), to po zastosowaniu rekurencji (i dzięki niej odłożenia wykonania operacji wyświetlenia cyfry) na ekranie cyfry wypisują się od

(23)

23

Funkcje rekurencyjne - przykład: zapis binarny

//wyświetla reprezentacja binarna liczby–wersja 2 void repr2(unsigned int n)

{ if(n<2) cout<<n;//jeśli jednocyfrowa //w syst. dwójkowym czyli 0 lub 1 else

{

repr2(n/2);

//przed wyświetleniem ostatniej

//cyfry wyświetlamy cyfry liczby n/2 cout<<n%2;//wyświetlamy ostania cyfra }

}

(24)

Funkcje rekurencyjne - przykład: zapis binarny Np repr2(6):

//6 nie jest <=1 wiec:

repr2(6/2);//3, wywołanie rekurencyjne repr2(3):

//3 nie jest <=1 wiec:

repr2(3/2);//1 wywołanie rekurencyjne repr2(1):

//1<=1 – war Stopu spełniony cout<<1;

cout<<3%2;//1 cout<<6%2;//0

(25)

25

Wyszukiwanie liniowe w tablicy

Przeszukiwanie liniowe (lub wyszukiwanie sekwencyjne) to

najprostszy algorytm wyszukiwania informacji zapisanych w tablicy.

Polega na porównywaniu szukanego elementu z kolejnymi

elementami tablicy – wyszukiwanie kończy się powodzeniem, gdy zostanie znaleziony szukany element, albo niepowodzeniem, gdy zostaną przejrzane wszystkie elementy.

Liczba koniecznych porównań zależy wprost od położenia szukanego elementu w sekwencji danych – wynosi od 1 do n, gdzie n to całkowita liczba elementów. Algorytm ma złożoność O(n).

Wyszukiwanie liniowe może być jedynym sposobem wyszukiwania, gdy nie wiadomo niczego na temat kolejności kluczy.

Dla dużej liczby danych algorytm jest bardzo nieefektywny, jednak gdy danych jest względnie mało, może być z powodzeniem

stosowany.

(26)

Wyszukiwanie liniowe w tablicy

//szukamy el x w n-elem. tablicy tab

//zwracamy indeks pierwszego wystąpienia //elementu x lub -1 jeśli go nie ma

int wyszukiwanieLin(int tab[],int n, int x) {

//przechodzimy wzdłuż tablicy for(int i=0; i < n;i++)

{

if(tab[i]==x)//jeśli jest szukany element return i;//zwracamy jego indeks

//i kończymy funkcję }

//przeszliśmy cala tablice (nie było x)

(27)

27

Wyszukiwanie liniowe w tablicy

int main()

{ int t[]={2,4,6,1,4};//tablica nieposortowana int sz = 4;//elem szukany

int p = wyszukiwanieLin(t, 5, sz);

if (p!= -1)

cout<<"W tablicy jest " << sz <<" na pozycji o indeksie "

<< p <<endl;

else

cout<<"W tablicy nie ma elementu: "

<< sz <<endl;

}

Cytaty

Powiązane dokumenty

ni 9,12 mln ha, tj. Dalej w komunikacie ogłoszono, że powierzchnia obsianych obsza- rów upraw ozimych może być w 2013 r. W bieżącym sezonie, obszary upraw pszenicy ozimej skur-

Przykład 3.18: Relacja niewiększości ≤ w (dowolnym) niepustym zbiorze liczb rzeczywistych liniowo porządkuje ten zbiór... Działania na relacjach Ponieważ relacje

[r]

Jeszcze raz korzystając z powyższego faktu, widzimy, że cała rzecz sprowadza się więc do ciągłości funkcji stałej i tożsamościowej x 7→ x, a to jest

Na początku podajmy komunikat, do czego jest nasz program (kategoria wygląd).. W kolejnym kroku z kategorii czujniki wybierzemy

Na początek musimy stworzyd dwie zmienne, które będą pamiętad liczby wprowadzone przez użytkownika.. Musimy w kategorii zmienne kliknąd w przycisk &gt; Utwórz zmienną

Autor: Małgorzata Paszyńska © Copyright by Nowa Era Sp..

Ile może wynosić miara zewnętrzna Lebesgue’a zbioru Vitaliego?.