Anatomia definicji rekursywnej int silnia(intn){ if(n==0) return 1; else return n*silnia(n-1); PRZYPADEK BAZOWY PRZYPADEK REKURSYWNY Definicja rekursywna musi zawierać przypadek bazowy, czyli kod bez wywołania rekursywnego, wykonywany dla bardzo małych argumentów. Kod dla przypadków rekursywnych(może ich być więcej niż jeden), wykonywany dla pozostałych argumentów, składa się z wywołań rekursywnych dla mniejszych argumentów, oraz słowa wiążącego, zawierającego przejście od wyniku funkcji dla mniejszych argumentów do wyniku dla większych argumentów. Anatomia definicji rekursywnej Wykład 7. REKURSJA, str. Przykład: (ciąg Fibonacciego) F(0) def =0 F(1) def =1 F(n+) def =F(n+1)+F(n) 0 1 1 3 5 8 13 1 34 Rozrastanie się drzewa: Każdagałąźwjednymroku wypuszcza gałązkę potomną,awnastępnymodpoczywa. Rok6 8 Rok5 5 Rok4 3 Rok3 Rok 1
Anatomia definicji rekursywnej Przykład: (ciąg Fibonacciego) F(0) def =0 F(1) def =1 F(n+) def =F(n+1)+F(n) int Fib(intn) { if(n<=1) returnn; else return Fib(n-1)+Fib(n-); Powyższa definicja nie mówi wprost, czemu jest równy kolejny wyraz ciągu Fibonacciego; tylko jak przejść od wyrazów mniejszych do większych. Za resztę odpowiada mechanizm rekursji. Anatomia definicji rekursywnej Wykład 7. REKURSJA, str. 4 Przykład: (maksimum system pucharowy) double max(doublea[],intn,intk) { /* wyszukuje największą liczbę w ciągu a[n]...a[k-1]*/ int p; double m1,m; if(k-n==1) returna[n]; else { p=(n+k)/; m1=max(a,n,p); m=max(a,p,k); if(m1>m) returnm1; else return m; Powyższa definicja nie mówi wprost, jak znaleźć maksimum; tylko jak z maksimów ciągów krótszych zrobić maksimum ciągu dłuższego. Za resztę odpowiada mechanizm rekursji.
Anatomia definicji rekursywnej Przykład: (maksimum system pucharowy) 0 1 3 4 5 6 a: 17.0-1.5 33.3 0.01-15.8 3.15 1.34 0+ 0+4 0+9 4+9 +4 4+6 7.68 6+9 8 5.99 7+9 17.0-1.5 33.3 0.01-15.8 3.15 1.34.68 5.99 {{ {{{{{{ 17.0 33.3 3.15 5.99 {{ 5.99 {{ {{ 33.3 {{ 3.15 33.3 9 Zasady indukcji matematycznej Wykład 7. REKURSJA, str. 6 Zupełna: W(0) ( k<n W(k)) W(n) n W(n) Klasyczna: P(0) P(n) P(n+1) n P(n) TWIERDZENIE: Powyższe dwie zasady indukcji są równoważne. Dowód wynikania Z K: Załóżmy, że zachodzi P(0) oraz P(n) P(n+1) Wtedyzachodzirównież( k<n+1 P(k)) P(n+1),botaimplikacjajest słabszaodp(n) P(n+1).WobectegozregułyZmamy n P(n).
Zasady indukcji matematycznej Zupełna: W(0) ( k<n W(k)) W(n) n W(n) Klasyczna: P(0) P(n) P(n+1) n P(n) TWIERDZENIE: Powyższe dwie zasady indukcji są równoważne. Dowód wynikania Z K: Załóżmy,żezachodziW(0)oraz ( k<n W(k)) W(n) ( ). def Oznaczmy: P(n) k<n+1 W(k).WtedyP(0) W(0)oraz P(n) P(n)& k<n+1 W(k) ( ) P(n)&W(n+1) P(n+1) ZregułyKmamy n k<n+1 W(k),cojestrównoważne n W(n). Indukcja w logice filozoficznej Wykład 7. REKURSJA, str. 8 Wnioskowanie dedukcyjne: CODZIENNIE WSCHODZI SŁOŃCE 7 XII 008 wzeszło Słońce 6 XII 009 wzeszło Słońce 7 XII 009 wzeszło Słońce Wnioskowanie indukcyjne: CODZIENNIE WSCHODZI SŁOŃCE 7 XII 008 wzeszło Słońce 7 XII 009 wzeszło Słońce 6 XII 009 wzeszło Słońce
Indukcja w logice filozoficznej Rozumowanie indukcyjne: wyciąganie wniosków ogólnych ze szczególnych przypadków; np. wyprowadzanie praw natury z pojedynczych eksperymentów. Żeby indukcja była poprawną metodą wnioskowania, potrzebne są dodatkowe założenia. Klasyczna indukcja matematyczna oraz indukcja zupełna są poprawnymi metodami wnioskowania o własnościach liczb naturalnych; już dla liczb całkowitych zawodzą. Przy zastosowaniach informatycznych miara wielkości problemu musi być naturalna;np.wprzykładziemaxbyłaniąwielkośćk n. Badanie własności funkcji rekursywnej Wykład 7. REKURSJA, str. 10 double max(doublea[],intn,intk) { int p; double m1,m; if(k-n==1) returna[n]; else { p=(n+k)/; m1=max(a,n,p); m=max(a,p,k); if(m1>m) returnm1; else return m; TWIERDZENIE: Jeślin+1 k,to max(a,n,k) jest największą liczbą spośród a[n]...a[k-1]. Dowóddlaprzypadkuk n=1: Wtedyk=n+1,więcchodziociąga[n]...a[n],zawierającytylko jeden element a[n]; więc to ten element jest największy. I ten właśnie element funkcja oddaje.
Badanie własności funkcji rekursywnej double max(doublea[],intn,intk) { int p; double m1,m; if(k-n==1) returna[n]; else { p=(n+k)/; m1=max(a,n,p); m=max(a,p,k); if(m1>m) returnm1; else return m; TWIERDZENIE: Jeślin+1 k,to max(a,n,k) jest największą liczbą spośród a[n]...a[k-1]. Dowóddlaprzypadkuk n>1: Wtedyk n+,więcn<p<k. m 1 jestnajwiększaspośróda[n]...a[p-1]a m jestnajwiększaspośróda[p]...a[k-1], więc większa z nich jest największa w całym a[n]...a[k-1]. I tą właśnie liczbę funkcja oddaje. Schemat dowodu Udowodniliśmy, że Wykład 7. REKURSJA, str. 1 wprzypadkubazowymk m=1wywołaniefunkcjimax(a,m,k) działa poprawnie, wprzypadkurekursywnymk m>1wywołaniefunkcjimax(a,m,k) poprawnie konstruuje wynik z wyników wywołań max(a,m,k ), dla którychk m <k m. Stąd wyciągnęliśmy wniosek, że wywołanie max(a,m,k) działa poprawnie dladowolnegok m 1. W przypadku rekursywnym: założyliśmy, że poprawnie działają wewnętrzne wywołania max(a,m,k )dlak m <k m; z tego wywnioskowaliśmy, że poprawnie działa wywołanie max(a,m,k).
Zamiana iteracji for na rekursję Iteracja for daje się w zasadzie zastąpić wywołaniem funkcji rekursywnej zdefiniowanej bez użycia for-ów i while-ów: for(i=a; i<b; i=i+1) komenda; void ff(inti) { if(i<b) { komenda; ff(i+1); ff(a); Iteracja idzie naprzód, rekursja idzie w głąb. Efektjestwzasadzietensam. Zamiana iteracji for na rekursję Wykład 7. REKURSJA, str. 14 Przykład: (wypełnianie tablicy liczbami) for(i=0; i<100; i=i+1) tab[i]=i; void ff(inti) { if(i<100) { tab[i]=i; ff(i+1); ff(0); Dla dowolnej liczby naturalnej i, wywołanie ff(i) powoduje wypełnienie liczbami tablicy tab od pozycji i do pozycji 99 włącznie. Wywołanie ff(0) powoduje wypełnienie liczbami tablicy tab od pozycji 0 do pozycji 99 włącznie.
Zamiana pętli while na rekursję Pętla while daje się w zasadzie zastąpić wywołaniem funkcji rekursywnej zdefiniowanej bez użycia for-ów i while-ów: while(warunek) komenda; void ww() { if(warunek) { komenda;ww(); ww(); Pętla idzie naprzód, rekursja idzie w głąb. Efektjestwzasadzietensam. Zamiana pętli while na rekursję Wykład 7. REKURSJA, str. 16 Przykład: (szukanie liczby pierwszej większej od danej n) int k,i; int k,i; k=n+1; i=; while(i<k) if(k%i==0) { k=k+1; i=; else i=i+1; void ww() { if(i<k) { if(k%i==0) { k=k+1; i=; else i=i+1; ww(); k=n+1; i=; ww();
Zamiana pętli while na rekursję Przykład: (szukanie liczby pierwszej większej od danej n) int k,i; k=n+1; i=; while(i<k) if(k%i==0) { k=k+1; i=; else i=i+1; int ww(intk,inti) { if(i==k) returnk; else if(k%i==0) return ww(k+1, ); else return ww(k, i+1); ww(n+1,); Dladowolnejparyliczbkii,jeślikniedzielisięprzezżadnąliczbęmiędzy a i 1 włącznie, to ww(k,i) jest najmniejszą liczbą pierwszą niemniejszą niżk. ww(n + 1,) jest najmniejszą liczbą pierwszą większą od n. Zamiana pętli while na rekursję Wykład 7. REKURSJA, str. 18 Efekt jest w zasadzie ten sam. Wyjątek od zasady: Każde wywołanie rekursywne zajmuje kawałek pamięci, więc bardzo głęboka rekursja kończy się błędem wyczerpania pamięci. Przykład: while(1==1) a=a+1; void qq() { if(1==1) { a=a+1;qq(); qq(); Pętla while: nieskończone działanie. Funkcja qq: Segmentation fault Naruszenie ochrony pamięci
Zamiana pętli while na rekursję każdy rodzaj pętli można łatwo zastąpić rekursją; przy takim zastępowaniu, jakie zademonstrowano na poprzednich slajdach, otrzymuje się tylko t.zw. rekursję ogonową(tail-recursion): w ciele funkcji nic się już po wywołaniu rekursywnym nie dzieje; siła rekursji jako metody programowania bierze się z możliwości wykonywania działań po wywołaniu rekursywnym; a więc nie z rekursji ogonowej; wiele problemów programistycznych prowadzi w sposób naturalny do rozwiązania rekursywnego; przy pewnych problemach pętla a przy innych rekursja jest właściwym narzędziem; wybór należy do programisty; zastąpienie rekursji pętlą jest zwykle trudne wymaga skomplikowanego zarządzania pamięcią komputera. Wykład 7. REKURSJA, str. 0 Sortowanie rekursywne scalanie(merge-sort) double a[n]; void mergesort(intp,intq) { // porządkuje elementy tablicy //oda[p]doa[q-1]włącznie intr; if(p+1<q) { r=(p+q)/; mergesort(p,r); mergesort(r,q); merge(p,r,q); Funkcja merge scala niemalejące ciągi a[p]...a[r-1] i a[r]...a[q-1] wniemalejącyciąga[p]...a[q-1].
Sortowanie rekursywne scalanie(merge-sort) a: 0 17.0 1-1.5 33.3 3 0.01 4-15.8 5 3.15 6 1.34 7.68 8 5.99 9-1.5 17.0 0.01 33.3-15.8 3.15 1.34.68 5.99-1.5 0.01 17.0 33.3-15.8 3.15 1.34.68 5.99-1.5 0.01 17.0 33.3-15.8 1.34.68 5.99 3.15-15.8-1.5 0.01 1.34.68 5.99 17.0 3.15 33.3 Wykład 7. REKURSJA, str. Sortowanie rekursywne scalanie(merge-sort) double a[n]; void merge(intp,intr,intq) { // scala niemalejące ciągi a[p]...a[r-1] i //a[r]...a[q-1]wniemalejącyciąga[p]...a[q-1] inti,j,k; doublepom[n]; i=p; j=r; k=p; while(i<r&&j<q) { if(a[i] <= a[j]) { pom[k]=a[i]; i=i+1; k=k+1; else { pom[k]=a[j]; j=j+1; k=k+1; while(i<r) { pom[k]=a[i]; i=i+1; k=k+1; while(j<q) { pom[k]=a[j]; j=j+1; k=k+1; for(k=p; k<q; k=k+1) a[k]=pom[k];
Sortowanie przez scalanie(merge-sort) symulacja void mergesort(intp,intq) { intr; if(p+1<q) { r=(p+q)/; mergesort(p,r); mergesort(r,q); merge(p,r,q); Symulacja działania funkcji: mergesort(0,9); = = mergesort(0,4); mergesort(4,9); merge(0,4,9); = mergesort(0,); mergesort(,4); merge(0,,4); = mergesort(4,6); mergesort(6,9); merge(4,6,9); merge(0,4,9); = mergesort(0,1); mergesort(1,); merge(0,1,); = mergesort(,3); mergesort(3,4); merge(,3,4); merge(0,,4); = mergesort(4,5); mergesort(5,6); merge(4,5,6); = mergesort(6,7); mergesort(7,9); merge(6,7,9); merge(4,6,9); merge(0,4,9); = merge(0,1,); merge(,3,4); merge(0,,4); merge(4,5,6); = mergesort(7,9); merge(6,7,9); merge(4,6,9); merge(0,4,9); = Wykład 7. REKURSJA, str. 4 Sortowanie rekursywne scalanie(merge-sort) Oszacowanie czasu działania funkcji mergesort: Na scalenie dwóch ciągów długości n funkcja merge potrzebuje: gdziec 1 jestpewnąstałą. n porównań 4n przepisań razem c 1 n Załóżmy,żenjestpotęgą;iżedladowolnegok,T(k)jestczasem,jakiego mergesort potrzebuje na uporządkowanie ciągu długości k. T(n)= { c0 dlan=1 T( n )+c 1 n dlan>1 co się sprowadza do T(n) = c 1 n log n+c 0 n Dowód:indukcja(dlan potęgi).
Sortowanie rekursywne scalanie(merge-sort) Oszacowanie czasu działania funkcji mergesort: czas sortowania czas sortowania n bąbelkowegon przezscalanienlog n szybciejrazy czyli około: czyli około: 64 4096 384 11 104 1 000 000 10 000 100 10 6 10 1 10 7 50000 10 9 10 18 3 10 10 3 10 7 Wykład 7. REKURSJA, str. 6 Sortowanie rekursywne quick-sort-light double a[n]; void quicksort(intp,intq) { // porządkuje elementy tablicy //oda[p]doa[q-1]włącznie intk,l; doubler; if(p+1<q) { r=a[(p+q)/]; // rozdzielić ciąg a[p]...a[q-1] na trzy ciągi: // ciąga[p]...a[k-1]liczb<r // ciąga[k]...a[l-1]liczb=r // ciąga[l]...a[q-1]liczb>r quicksort(p,k); quicksort(l,q);