o ciąg
Transkrypt
o ciąg
Anatomia definicji rekursywnej int silnia (int n) { 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. Wykład 7. REKURSJA, str. 2 Anatomia definicji rekursywnej Przykład: (ciąg Fibonacciego) M def F (0) = 0 F (1) def =1 F (n + 2) def = F (n + 1) + F (n) Rozrastanie się drzewa: Każda gałąź w jednym roku wypuszcza gałązkę potomną, a w następnym odpoczywa. 0 1 1 2 3 5 8 13 21 34 Rok 6 8 Rok 5 5 Rok 4 3 Rok 3 2 Rok 2 1 Anatomia definicji rekursywnej Przykład: (ciąg Fibonacciego) M def F (0) = 0 def F (1) =1 F (n + 2) def = F (n + 1) + F (n) int Fib (int n) { if (n <= 1) return n; else return Fib(n-1)+Fib(n-2); } 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. Wykład 7. REKURSJA, str. 4 Anatomia definicji rekursywnej Przykład: (maksimum — system pucharowy) M max (double a[ ], int n, int k) { /* wyszukuje największą liczbę w ciągu int p; double m1,m2; if (k-n == 1) return a[n]; else { p = (n+k)/2; m1 = max(a, n, p); m2 = max(a, p, k); if (m1 > m2) return m1; else return m2; } double a[n]...a[k-1] */ } 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) M 1 0 2 3 a: 17.0 -1.25 33.3 4 5 6 0.01 -15.8 23.15 1.34 0+9 7 8 9 2.68 5.99 2 0+4 2 0+2 2 17.0 | | -1.25 {z 17.0 | } | 33.3 {z 33.3 2+4 0.01 {z 33.3 2 4+6 2 2 -15.8 } | } | 4+9 23.15 {z 23.15 6+9 1.34 } | {z 23.15 {z 33.3 2 2.68 | 7+9 2 5.99 {z } 5.99 {z } 5.99 } } Wykład 7. REKURSJA, str. 6 Zasady indukcji matematycznej Zupełna: Klasyczna: W (0) (∀k<n W (k)) ⇒ W (n) ∀n W (n) P (0) P (n) ⇒ P (n + 1) ∀n P (n) TWIERDZENIE: M 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) Wtedy zachodzi również (∀k<n+1 P (k)) ⇒ P (n+1), bo ta implikacja jest słabsza od P (n) ⇒ P (n + 1). Wobec tego z reguły Z mamy ∀n P (n). Zasady indukcji matematycznej Zupełna: Klasyczna: W (0) (∀k<n W (k)) ⇒ W (n) ∀n W (n) P (0) P (n) ⇒ P (n + 1) ∀n P (n) TWIERDZENIE: M Powyższe dwie zasady indukcji są równoważne. Dowód wynikania Z⇐K: Załóżmy, że zachodzi W (0) oraz (∀k<n W (k)) ⇒ W (n) (∗) . def Oznaczmy: P (n) ⇐⇒ ∀k<n+1 W (k). Wtedy P (0) ⇐⇒ W (0) oraz (∗) P (n) ⇐⇒ P (n) & ∀k<n+1 W (k) ⇒ P (n) & W (n+1) ⇐⇒ P (n+1) Z reguły K mamy ∀n ∀k<n+1 W (k), co jest równoważne ∀n W (n). Wykład 7. REKURSJA, str. 8 Indukcja w logice filozoficznej Wnioskowanie dedukcyjne: CODZIENNIE WSCHODZI SŁOŃCE 7 XII 2008 wzeszło Słońce ? 6 XII 2009 wzeszło Słońce R 7 XII 2009 wzeszło Słońce Wnioskowanie indukcyjne: CODZIENNIE WSCHODZI SŁOŃCE 6 I 7 XII 2008 wzeszło Słońce 7 XII 2009 wzeszło Słońce 6 XII 2009 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. w przykładzie max była nią wielkość k − n. Wykład 7. REKURSJA, str. 10 Badanie własności funkcji rekursywnej double max (double a[ ], int n, int k) { int p; double m1,m2; if (k-n == 1) return a[n]; else { p = (n+k)/2; m1 = max(a, n, p); m2 = max(a, p, k); if (m1 > m2) return m1; else return m2; }} TWIERDZENIE: Jeśli n + 1 ¬ k, to max(a,n,k) jest największą liczbą spośród a[n]...a[k-1] . Dowód dla przypadku k − n = 1: Wtedy k = n + 1, więc chodzi o ciąg a[n]...a[n], zawierający tylko 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 (double a[ ], int n, int k) { int p; double m1,m2; if (k-n == 1) return a[n]; else { p = (n+k)/2; m1 = max(a, n, p); m2 = max(a, p, k); if (m1 > m2) return m1; else return m2; }} TWIERDZENIE: Jeśli n + 1 ¬ k, to max(a,n,k) jest największą liczbą spośród a[n]...a[k-1] . Dowód dla przypadku k − n > 1: Wtedy k n + 2, więc n < p < k. m1 jest największa spośród a[n]...a[p-1] a m2 jest największa spośród a[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. Wykład 7. REKURSJA, str. 12 Schemat dowodu Udowodniliśmy, że • w przypadku bazowym k − m = 1 wywołanie funkcji max(a,m,k) działa poprawnie, • w przypadku rekursywnym k − m > 1 wywołanie funkcji max(a,m,k) poprawnie konstruuje wynik z wyników wywołań max(a,m’,k’), dla których k ′ − m′ < k − m. Stąd wyciągnęliśmy wniosek, że wywołanie max(a,m,k) działa poprawnie dla dowolnego k − m 1. W przypadku rekursywnym: • założyliśmy, że poprawnie działają wewnętrzne wywołania max(a,m’,k’) dla k ′ − 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 (int i) { if (i<b) { komenda; ff(i+1); } } ··· ff(a); Iteracja idzie „naprzód”, rekursja idzie „w głąb”. Efekt jest w zasadzie ten sam. Wykład 7. REKURSJA, str. 14 Zamiana iteracji for na rekursję Przykład: (wypełnianie tablicy liczbami) M for (i=0; i<100; i=i+1) tab[i]=i; −→ void ff (int i) { 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”. Efekt jest w zasadzie ten sam. Wykład 7. REKURSJA, str. 16 Zamiana pętli while na rekursję Przykład: (szukanie liczby pierwszej większej od danej n) M int k, i; int k, i; k=n+1; i=2; ··· while (i<k) if (k%i == 0) { k=k+1; i=2; } else i=i+1; −→ void ww () { if (i<k) { if (k%i == 0) { k=k+1; i=2; } else i=i+1; ww(); } } ··· k=n+1; i=2; ww(); Zamiana pętli while na rekursję Przykład: (szukanie liczby pierwszej większej od danej n) M int ww (int k, int i) { int k, i; if (i == k) return k; k=n+1; i=2; else if (k%i == 0) ··· return ww(k+1, 2); while (i<k) if (k%i == 0) { else return ww(k, i+1); k=k+1; i=2; } } ··· else i=i+1; ww(n+1, 2); −→ Dla dowolnej pary liczb k i i, jeśli k nie dzieli się przez żadną liczbę między 2 a i − 1 włącznie, to ww(k,i) jest najmniejszą liczbą pierwszą niemniejszą niż k. ww(n + 1,2) jest najmniejszą liczbą pierwszą większą od n. Wykład 7. REKURSJA, str. 18 Zamiana pętli while na rekursję „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: M while (1 == 1) a = a+1; −→ Pętla while: nieskończone działanie. Funkcja qq: „Segmentation fault” void qq() { if (1 == 1) { a = a+1; qq(); } } ··· qq(); „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. 20 Sortowanie rekursywne — scalanie (merge-sort) double a[N]; mergesort (int p, int q) { // porządkuje elementy tablicy // od a[p] do a[q-1] włącznie int r; if (p+1 < q) { r = (p+q)/2; mergesort(p,r); mergesort(r,q); merge(p,r,q); } void } Funkcja merge scala niemalejące ciągi a[p]...a[r-1] i a[r]...a[q-1] w niemalejący ciąg a[p]...a[q-1]. Sortowanie rekursywne — scalanie (merge-sort) 0 1 2 a: 17.0 -1.25 33.3 3 4 5 6 7 8 0.01 -15.8 23.15 1.34 2.68 5.99 -1.25 17.0 0.01 33.3 -15.8 23.15 1.34 2.68 5.99 -1.25 0.01 17.0 33.3 -15.8 23.15 1.34 2.68 5.99 -1.25 0.01 17.0 33.3 -15.8 1.34 2.68 5.99 23.15 1.34 17.0 23.15 33.3 -15.8 -1.25 0.01 2.68 5.99 9 Wykład 7. REKURSJA, str. 22 Sortowanie rekursywne — scalanie (merge-sort) double a[N]; merge (int p, int r, int q) { // scala niemalejące ciągi a[p]...a[r-1] i // a[r]...a[q-1] w niemalejący ciąg a[p]...a[q-1] int i, j, k; double pom[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]; void } Sortowanie przez scalanie (merge-sort) — symulacja void mergesort (int p, int q) { int r; if (p+1 < q) { r = (p+q)/2; 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,2); mergesort(2,4); merge(0,2,4); = mergesort(4,6); mergesort(6,9); merge(4,6,9); merge(0,4,9); = = = = mergesort(0,1); mergesort(2,3); mergesort(4,5); mergesort(6,7); mergesort(1,2); mergesort(3,4); mergesort(5,6); mergesort(7,9); merge(0,1,2); merge(2,3,4); merge(0,2,4); merge(4,5,6); merge(6,7,9); merge(4,6,9); merge(0,4,9); = merge(0,1,2); merge(2,3,4); merge(0,2,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. 24 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: gdzie c1 jest pewną stałą. 2n porównań 4n przepisań razem c1 n Załóżmy, że n jest potęgą 2; i że dla dowolnego k, T (k) jest czasem, jakiego mergesort potrzebuje na uporządkowanie ciągu długości k. T (n) = ( c0 dla n = 1 n 2 · T ( 2 ) + c1 · n dla n > 1 co się sprowadza do T (n) = c1 · n · log2 n + c0 · n Dowód: indukcja (dla n — potęgi 2). Sortowanie rekursywne — scalanie (merge-sort) Oszacowanie czasu działania funkcji mergesort: n czas sortowania bąbelkowego n2 czyli około: czas sortowania przez scalanie n log2 n czyli około: 64 4096 384 11 1024 1 000 000 10 000 100 106 1012 2 · 107 50 000 109 1018 3 · 1010 3 · 107 szybciej razy Wykład 7. REKURSJA, str. 26 Sortowanie rekursywne — quick-sort-light double a[N]; quicksort (int p, int q) { // porządkuje elementy tablicy // od a[p] do a[q-1] włącznie int k,l; double r; if (p+1 < q) { r = a[(p+q)/2]; ··· // rozdzielić ciąg a[p]...a[q-1] na trzy ciągi: // ciąg a[p]...a[k-1] liczb < r // ciąg a[k]...a[l-1] liczb = r // ciąg a[l]...a[q-1] liczb > r ··· quicksort(p,k); quicksort(l,q); } void }