1 Podstawy algorytmiki i programowania - wykład 3 Funkcje rekurencyjne Wyszukiwanie liniowe i binarne w tablicy 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.
2 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.
3 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;
4 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 wywołanie powrót 2*silnia(1) =2*1 1 powrót
Funkcje rekurencyjne - przykład n! 5
Funkcje rekurencyjne - przykład n! -iteracyjnie unsigned long long silniait(unsigned short n) 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
7 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. Podzielenie 18 przez 12 daje iloraz 1 i resztę równą 6. Ostatecznie podzielenie 12 przez 6 daje zerową resztę, co oznacza, że 6 jest największym wspólnym dzielnikiem 84 i 18. nwd(84,18)=nwd(18,12)=nwd(12,6)=nwd(6,0)=6
8 nwd(a,0) = a, Funkcje rekurencyjne - przykład nwd nwd(a,b) = nwd(b,a mod b) unsigned int nwd(unsigned int a,unsigned int b) if(b == 0) return a; 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.
9 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
Funkcje rekurencyjne - przykład nwd 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. 10
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; 11
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); 12
13 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: F 0 =0, F1=1, Fn=Fn-1+ Fn-2, n>=2. Rekurencja jest tu nieefektywna, bo zadanie o rozmiarze n jest dzielone na dwie części o rozmiarze bliskim n.
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; 14
15 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; cout << "Czas (s): " << ((float)(clock() - start))/clocks_per_sec << "Licznik = " <<licznik; // 331160281 cout<<"\nwersja iteracyjna "<<endl; start = clock(); cout << FibIt(40) << endl; cout << "Czas (s): " << ((float)(clock() - start))/clocks_per_sec;
16 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
17 (rys. [3]) Funkcje rekurencyjne - przykład: zapis binarny
18 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 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.*/
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 lewej do prawej. 19
20 Funkcje rekurencyjne - przykład: zapis binarny //wyswietla reprezentacja binarna liczby wersja 2 void repr2(unsigned int n) if(n<2) cout<<n;//jesli jednocyfrowa //w syst.dwojkowym czyli 0 lub 1 else repr2(n/2);//przed wyswietleniem ostatniej //cyfry wyswietlamy cyfry liczby n/2 cout<<n%2;//wyswietlamy ostania cyfra
21 Np repr2(6): Funkcje rekurencyjne - przykład: zapis binarny //6 nie jest <=1 wiec: repr2(6/2);//3, wowołanie rekurencyjne repr2(3): //3 nie jest <=1 wiec: repr2(3/2);//1 wowołanie rekurencyjne repr2(1): //1<=1 war Stopu spelniony cout<<1; cout<<3%2;//1 cout<<6%2;//0 Otrzymujemy: 110
22 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.
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 23 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) return -1;
24 Wyszukiwanie liniowe w tablicy int main() int t[]=2,4,6,1,4;//tablica posortowana int sz = 4;//elem szukany int p = wyszukiwanielin(t, 5, sz); if (p!= -1) else cout<<"w tablicy jest " << sz <<" na pozycji o indeksie "<< p <<endl; cout<<"w tablicy nie ma elementu: " << sz <<endl;
25 Wyszukiwanie binarne w tablicy Wyszukiwanie binarne algorytm opierający się na metodzie dziel i zwyciężaj, który w czasie logarytmicznym stwierdza, czy szukany element znajduje się w uporządkowanej tablicy i jeśli się znajduje, podaje jego indeks. Np. jeśli tablica zawiera milion elementów, wyszukiwanie binarne musi sprawdzić maksymalnie 20 elementów (log 2 1000000~20 ) w celu znalezienia żądanej wartości. Dla porównania wyszukiwanie liniowe wymaga w najgorszym przypadku przejrzenia wszystkich elementów tablicy.
PRZEBIEG ALGORYTMU: Wyszukiwanie binarne w tablicy 1)Wyznaczamy element środkowy zbioru. 2)Sprawdzimy, czy jest on poszukiwanym elementem. Jeśli tak, to element został znaleziony i możemy zakończyć poszukiwania. 3)Jeśli nie, to poszukiwany element jest albo mniejszy od elementu środkowego, albo większy. Ponieważ zbiór jest uporządkowany, to elementy mniejsze od środkowego będą leżały w pierwszej połówce zbioru, a elementy większe w drugiej połówce. 4)Zatem w następnym obiegu zbiór możemy zredukować do pierwszej lub drugiej połówki jest w nich o połowę mniej elementów. 5)Mając nowy zbiór postępujemy w sposób identyczny powracamy do 1) i znów wyznaczamy element środkowy, sprawdzamy, czy jest poszukiwanym elementem. Jeśli nie, to zbiór dzielimy znów na dwie połowy elementy mniejsze od środkowego i elementy większe od środkowego. Poszukiwania kontynuujemy w odpowiedniej połówce zbioru aż znajdziemy poszukiwany element lub do chwili, gdy po podziale połówka zbioru nie zawiera dalszych elementów. 26
Wyszukiwanie binarne w tablicy 27
Wyszukiwanie binarne w tablicy (rekurencyjnie) //---------Wyszukiwanie binarne wersja rekurencyjna //szukamy el x w tablicy tab posortowanej niemalejąco //zwracamy indeks el.x jeśli jest,a jeśli go nie ma -1 int binarne(int tab[],int poczatek, int koniec, int x) if (poczatek <= koniec)//są elem.tablicy do sprawdzenia int srodek = (poczatek+koniec)/2; if (x == tab[srodek])//znaleźliśmy element return srodek;//zwracamy jego indeks else if (x < tab[srodek])//szukamy w lewej połowie else return binarne(tab,poczatek,srodek-1,x); //szukamy w lewej połowie return binarne(tab,srodek+1,koniec,x); else //nie ma elementów do sprawdzania return -1; 28
29 Wyszukiwanie binarne w tablicy int main() int t[]=2,5,6,8,9;//tablica posortowana int sz=3;//elem szukany int p = binarne(t,0, 4, sz); if (p!= -1) else cout<<"w tablicy jest " << sz <<" na pozycji o indeksie "<< p <<endl; cout<<"w tablicy nie ma elementu: " << sz <<endl;
Wyszukiwanie binarne w tablicy (iteracyjnie) bool binarne(int t[], int n, int szuk,int &poz) int lewy = 0, prawy = n-1, sr; while (lewy <= prawy) sr = (lewy+prawy)/2; if (szuk < t[sr]) prawy = sr -1; else if (szuk > t[sr]) lewy = sr +1; else poz = sr; return true; return false; 30
31 Wyszukiwanie binarne w tablicy (iteracyjnie) int main() int t[]2,5,6,8,9;//tablica posortowana int n = 5; int sz = 3;//elem szukany int p; if (binarne(t,n,sz,p)) cout<<"w tablicy jest " << sz <<" na pozycji "<< p << endl; else cout<<"w tablicy nie ma elementu: " << sz << endl;