12. Rekurencja. Funkcja rekurencyjna funkcja, która wywołuje samą siebie. Naturalne postępowanie: np. zbierając rozsypane pionki do gry podnosi się zwykle pierwszy, a potem zbiera się resztę w ten sam sposób. Schemat funkcji: void rekur (<listaargumentow>) <instrukcje>; if ( <warunek>) rekur (<listaargumentow2>) <instrukcje> UWAGA Trzeba bardzo dokładnie ustalić <warunek>, żeby mieć pewność, że ciąg wywołań się zakończy. 12.1 Wykorzystanie stosu. Kolejne wywołania z parametrami są umieszczane na stosie. Rekurencja jawna jest możliwa tylko w językach obsługujących stos wywołań! Przykład 1 (rek_p1) #include <iostream> using namespace std; const int NMAKS = 15; long int SILNIA(int n) cout<<"n = "<<n<<endl; if(n<=1) return 1; else return n * SILNIA(n-1); int main () int liczba; cout<<"jakiej liczby policzymy silnie? "; cin>>liczba; if (liczba > NMAKS) cout<<"******niestety za duza liczba"<<endl; else if (liczba<0) cout<<"******liczba ujemna"<<endl; else cout<<"silnia liczby "<<liczba<<" wynosi " << SILNIA(liczba)<<endl; return 0; 1
Realizacja 2
12.2 Linijka. Strategia: dziel i zwyciężaj Rysowanie podziałki na linijce: 1. zaznacz dwa końce; 2. znajdź i zaznacz środek; 3. wykonaj to samo dla lewej połowy podziału 4. wykonaj to samo dla prawej połowy podziału /* ruler.cpp - uzycie rekurencji do dzielenia linijki */ #include <iostream> using namespace std; const int Len = 66; const int Divs = 6; void subdivide(char ar[], int low, int high, int level); int main() char ruler[len]; int i, j; int max, min; for (i = 1; i < Len - 2; i++) ruler[i] = ' '; ruler[len - 1] = '\0'; max = Len - 2; min = 0; ruler[min] = ruler[max] = ' '; cout<<ruler<<endl; for (i = 1; i <= Divs; i++) subdivide(ruler,min,max, i); cout<< ruler<<endl; for (j = 1; j < Len - 2; j++) ruler[j] = ' '; /* zerowanie linijki */ return 0; void subdivide(char ar[], int low, int high, int level) int mid; if (level == 0) /* zmienna level kontroluje poziom rekurencji */ /* przy każdym wywołaniu zmniejszana o 1 */ return; mid = (high + low) / 2; ar[mid] = ' '; subdivide(ar, low, mid, level - 1); subdivide(ar, mid, high, level - 1); 3
12.3 Ciągi zdefiniowane rekurencyjnie. Ciąg Fibonacciego Obliczanie parametrów ciągu tego i innych, podobnych typów, wykonane bezpośrednio na podstawie wzoru matematycznego, może powodować problemy w postaci zbyt wielu dublujących się obliczeń. Ciąg Fibonacciego jest zdefiniowany rekurencyjnie, w sposób analogiczny do definicji funkcji silnia: F(0) = 1 F(1) = 1 F(n) = F(n-1) + F(n-2) Zadanie obliczania elementów takiego ciągu można w języku C zapisać niemal dokładnie tak samo, jak wyraża to wzór definicyjny: unsigned long int FIB( int n ) if (n<2) return 1; else return FIB(n-1) + FIB(n-2); Schemat wywołań rekurencyjnych, jak wykaże prosta analiza kodu, doprowadzi do następującego drzewa wywołań: FIB(4) FIB(3) FIB(2) FIB(2) FIB(1) FIB(1) FIB(0) FIB(1) FIB(0) Drzewo wywołań funkcji FIB dla parametru 4. Gałęzie zaznaczone kolorem wykonują się dwa razy. Zupełnie niepotrzebnie. W sumie, wywołanie funkcji FIB dla większych parametrów n, spowoduje że wykonane zostanie w przybliżeniu 2n obliczeń, co jest z oczywistych powodów nieefektywne. Dlatego należy pamiętać, że nie jest dobrą metodą programowanie rekurencyjne tam, gdzie wystarczą proste funkcje iteracyjne. 4
Przykład 3 /* Fibonacii.cpp - uzycie rekurencji do wyliczania elementów ci¹gu Fibonacci'ego. */ #include <iostream> using namespace std; unsigned long int FIB( int n ); unsigned long int FIB2( int n ); unsigned long int FIB3( int n ); int main() int zakres; int i; cout<<"ile elementow chcesz tworzyc? "; cin>>zakres; cout<<"********* Rekurencyjnie"<<endl; for (i=0; i<=zakres; ++i) cout<<" fib("<<i<<")= "<<FIB(i)<<endl; cout<<"********* Iteracyjnie"<<endl; FIB2(zakres); cout<<"********* Iteracyjnie bez przechowywania"<<endl; FIB3(zakres); return 0; unsigned long int FIB( int n ) if (n<2) return 1; else return FIB(n-1) + FIB(n-2); unsigned long int FIB2( int n ) int i; unsigned long int *fibtab= new unsigned long int[n+1]; if (n<2) return 1; else fibtab[0]=fibtab[1]=1; for (i=2; i<=n; ++i) fibtab[i]=fibtab[i-1]+fibtab[i-2]; cout<<"fib2("<<i<<") = "<< fibtab[i]<<endl; return fibtab[n]; unsigned long int FIB3( int n ) int i; /* Jeżeli nie trzeba przechowywac wynikow posrednich */ long an, an1, an2; an1 = an2 = 1; 5
for (i = 2; i<=n; ++i) an = an1 + an2; cout<<"fib3("<<i<<") = "<< an<<endl; an2 = an1; an1 = an; return an; 12.4 Wieże Hanoi. Wieże Hanoi problem polegający na odbudowaniu, z zachowaniem kształtu, wieży z krążków o różnych średnicach (popularna dziecięca zabawka), przy czym podczas przekładania wolno się posługiwać buforem (reprezentowanym w tym przypadku przez dodatkowy słupek), jednak przy ogólnym założeniu, że nie wolno kłaść krążka o większej średnicy na mniejszy ani przekładać kilku krążków jednocześnie. Jest to przykład zadania, którego złożoność obliczeniowa wzrasta niezwykle szybko w miarę zwiększania parametru wejściowego, tj. liczby elementów wieży. Dla n krążków złożoność wynosi: 2 n -1 Rysunek 1 Od lewej: słupek A z całą wieżą, pusty słupek B pełniący rolę bufora i pusty słupek docelowy C http://upload.wikimedia.org/wikipedia/commons/6/60/tower_of_hanoi_4.gif Wieże Hanoi można łatwo rozwiązać za pomocą prostego algorytmu rekurencyjnego lub iteracyjnego. Oznaczmy kolejne słupki literami A, B i C. Niech n będzie liczbą krążków, które chcemy przenieść ze słupka A na słupek C posługując się słupkiem B jako buforem. 6
Rozwiązanie rekurencyjne Algorytm rekurencyjny składa się z następujących kroków: 1. przenieś (rekurencyjnie) n-1 krążków ze słupka A na słupek B posługując się słupkiem C, 2. przenieś jeden krążek ze słupka A na słupek C, 3. przenieś (rekurencyjnie) n-1 krążków ze słupka B na słupek C posługując się słupkiem A. Przykładowa implementacja /* Problem wiez Hanoi. Trzy slupki oznaczone A,B i C. Program wyswietla kolejnosc ruchów - z ktorego na ktory slupek. */ #include <iostream> using namespace std; int NrRuchu = 1; void hanoi(int n, char A, char B, char C) /* przeklada n krazków z A korzystajac z B na C */ if (n > 0) hanoi(n-1, A, C, B); cout<<nrruchu++<<'.'<< A<<" -> "<< C<<endl; hanoi(n-1, B, A, C); int main() int LiczbaKrazkow; cout<<"podaj liczbe krazkow "; cin>>liczbakrazkow; while (LiczbaKrazkow<=0 LiczbaKrazkow>10) cout<<"bledna liczba klockow. Podaj jeszcze raz liczbe od 1 do 10." <<endl; cout<<"podaj liczbe krazkow "; cin>>liczbakrazkow; hanoi(liczbakrazkow, 'A', 'B', 'C'); return 0; 7
12.5 Analiza składniowa metodą zejść rekurencyjnych. Gramatyka bezkontekstowa wyrażeń arytmetycznych: <wyrażenie> ::= <składnik> + <wyrażenie> <składnik> - <wyrażenie> <składnik> <składnik> ::= <czynnik> * <składnik> <czynnik> /<składnik> <czynnik> <czynnik> ::= <liczba> (<wyrażenie>) Podstawy programowania. Wykład 08 rekurencja Dla każdego symbolu nieterminalnego (umieszczony w nawiasach <>) piszę funkcję. Przykład 5. Kalkulator wyrażeń arytmetycznych z nawiasami. #include <iostream> //w12p5.cpp using namespace std; /*<wyrazenie> ::= <skladnik> + <wyrazenie> <skladnik> - <wyrazenie> <skladnik> <skladnik> ::= <czynnik> * <skladnik> <czynnik> /<skladnik> <czynnik> ::= <liczba> (<wyrazenie>). */ typedef enum PlusSm,MinusSm,RazySm, DzielSm,LewyNawSm, PrawyNawSm, LiczbaSm, PustySm symbole; #define MAXBUF 101 struct symb symbole SymbNm; double wart; symbol; char buforek[maxbuf]; int pozbuf = 0; void scan() /* pobiera z bufora kolejny symbol */ int wart; char znak; while ((znak = buforek[pozbuf++])== ' '); /* zjadamy spacje z poczatku */ if (isdigit(znak)) symbol.symbnm = LiczbaSm; /* argumenty tylko calkowite */ wart = znak - '0'; while (isdigit(znak = buforek[pozbuf])) 8
wart = 10*wart+ (znak - '0'); pozbuf++; symbol.wart = wart; else switch (znak) case '(': symbol.symbnm = LewyNawSm; case ')': symbol.symbnm = PrawyNawSm; case '+': symbol.symbnm = PlusSm; case '-': symbol.symbnm = MinusSm; case '*': symbol.symbnm = RazySm; case '/': symbol.symbnm = DzielSm; case '\0': symbol.symbnm = PustySm; default: cout<<"nieznany znak "<< znak<<endl; double skladnik(); double czynnik(); double wyrazenie() double wart; Podstawy programowania. Wykład 08 rekurencja wart = skladnik(); if (symbol.symbnm == PlusSm) /* zjadam + */ wart += wyrazenie(); else if (symbol.symbnm == MinusSm) wart -= wyrazenie(); cout<<"wwwwwwwwwwwwyrazenie = "<< wart<<endl; return wart; double skladnik() double wart; 9
wart = czynnik(); if (symbol.symbnm == RazySm) wart *= skladnik(); else if (symbol.symbnm == DzielSm) wart /= skladnik(); cout<<"sssssssssssssskladnik = "<<wart<<endl; return wart; double czynnik () double wart; if (symbol.symbnm == LiczbaSm) wart = symbol.wart; else if (symbol.symbnm == LewyNawSm) wart = wyrazenie(); if (symbol.symbnm!= PrawyNawSm) cout<<"**** Spodziewany nawias zamykajacy"<<endl; wart = 0; else cout<<"$$$$$ czynnik wart = "<<wart<<endl; return wart; int main() int i=0; char c; cout<< "Podaj wyrazenie "; while ((c = cin.get())!= '\n')buforek[i++] = c; buforek[i]='\0'; cout<<"wczytano "<<buforek<<' '<<endl; symbol.wart = 0; cout<< "= "<< wyrazenie()<<endl; return 0; 10