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
}