Rekurencja Definicje rekurencyjne Definicja: Mówimy, iż ciąg jest zdefiniowany rekurencyjnie, jeżeli: (P) Określony jest pewien skończony zbiór wyrazów tego ciągu, zwykle jest to pierwszy wyraz tego ciągu lub kilka jego pierwszych wyrazów. (R) Pozostałe wyrazy ciągu są zdefiniowane za pomocą poprzednich wyrazów ciągu. Wzór definiujący ciąg w taki sposób nazywamy zależnością rekurencyjną. Przykład. Rekurencyjna definicja ciągu SILNI, inaczej rekurencyjna definicja silni: (P) silnia(0) = (R) silnia(n+) = (n+) silnia(n) dla każdego n N. Przykład. Rozważmy ciąg SUM (n) n i i. W celu napisania programu komputerowego obliczającego wartości ciągu (SUM) n N dla dużych n, należy zapisać ten ciąg rekurencyjnie: (P) SUM() = (R) SUM(n+) = SUM(n) + (n+) dla każdego n N. Wyrazy ciągu zdefiniowanego rekurencyjnie można obliczać na wiele rozmaitych sposobów. Przykładowo, za pomocą obliczeń iteracyjnych znajdowana jest wartość n-tego wyrazu a n w oparciu o wyliczone uprzednio wartości wszystkich wyrazów ciągu poprzedzających wyraz a n, tj. wyrazów a, a,, a n-. Znajomość tych wyrazów ciągu jest niezbędna do obliczenia wartości wyrazu a n. Przykładowo, w przypadku powyżej zaprezentowanego ciągu (SILNI), w celu obliczenia wyrazu SILNI(95), należy obliczyć uprzednio wyrazy SILNI(k) dla k =,,94, nawet, jeśli większość (lub nawet wszystkie) poprzedzające wyrazy tego ciągu nie będą nigdzie indziej wykorzystane. Okazuje się jednak, iż w niektórych przypadkach istnieje bardziej efektywny sposób obliczenia wyrazu ciągu a n. Otóż, w obliczeniach rekurencyjnych wartość wyrazu ciągu a n zależy od pewnych wyrazów, a te z kolei zależą o innych, itd. zatem może okazać się, iż wartość wyrazu ciągu a n zależy tylko od stosunkowo niewielkiej liczby wyrazów poprzednich, a to sprawia, iż pozostałe wyrazy poprzednie można by pominąć. Przykład. Niech będzie dany ciąg (a n ) n N zdefiniowany następująco: (0) = 0, () =, (n) ( n / ) ( n / 5 ) dla n nalizując definicję tego ciągu, jest oczywistym, iż opłacalnym jest obliczanie wartości jego elementów rekurencyjnie, a nie iteracyjnie. Przykładowo: (9) = (46) + (8) = [(3) + (9)] + [(9) + (3)] = (3) + (9) +(3) = [() + (4)] + [(4) + ()] + () + (0) = () + 3(4) + 3() + (0) = (5) + () + 3[() + (0)] + 3() + (0) = (5) + 4() + 3() + 4(0) = () + () + 4[() + (0)] + 3() + 4(0) = () + 8() + 8(0) = () + (0) + 8() + 8(0) = 9() + 9(0) Jak widać, wprawdzie w obliczeniach wartości wyrazu (9) korzystamy z wartości pośrednich (46), (3), (8), (), (9), (5), (4), (3), (), (), (0), lecz w końcowym etapie potrzebne nam są tylko wartości () i (0).
Przykład. Niech będzie dany ciąg (a n ) n N zdefiniowany następująco: a 0 = 0, a n+ = a n + n. Zamiast wyliczać poszczególne wyrazy ciągu, można wykazać, iż a n = n n dla każdego n N. zatem do obliczenia dowolnego wyrazu ciągu (a n ) n N nie potrzeba obliczać żadnego z uprzednich wyrazów. Jeśli obliczamy wyrazy ciągu rekurencyjnie, to taki sposób ich obliczania wymaga pamięci dla przechowywania wartości pośrednich, które zostają wywoływane, ale jeszcze nie zostały obliczone. Jednak w niektórych przypadkach, może się okazać, iż liczba miejsc pamięci potrzebnych do przechowywania tych pośrednich wartości będzie stosunkowo niewielka. Przykładowo, w przypadku obliczeń rekurencyjnych silni np. SILNI(4) = 4 SILNI(3) = SILNI() = 4 SILNI() = 4 jest potrzebny tylko jeden adres w pamięci do przechowywania pośredniej (nieznanej) wartości silni. Przykład. Innym przykładem ilustrującym tę własność, jest obliczanie wartości ciągu Fibonacciego, który definiuje się w następujący sposób: (P) FIB(0) =, FIB() = (R) FIB(n) = FIB(n ) + FIB(n ) dla n Początkowymi wyrazami ciągu Fibonacciego są:,,, 3, 5, 8, 3,, 34, 55, 89. Przykładowo, FIB(4) = FIB(3) FIB() = [FIB() + FIB()] + [FIB() + FIB(0)] = [FIB() + FIB(0)] + FIB() + FIB(0) = 3FIB() + FIB(0) = 5 Jak widać z powyższego potrzebne są tylko dwa adresy do przechowania dwóch wartości pośrednich. Największy wspólny dzielnik - lgorytm Euklidesa Definicja. Liczba całkowita k jest dzielnikiem liczby całkowitej m wtedy i tylko wtedy, gdy m jest wielokrotnością liczby k, tzn. wtedy, gdy m = k p dla pewnej liczby p Z. W takim przypadku mówimy, iż k dzieli m. Ponieważ 0 = k 0, a zatem każda liczba jest dzielnikiem 0. Ponadto liczby m i m mają te same dzielniki. Prawdziwe jest także stwierdzenie, iż k jest dzielnikiem liczby m wtedy i tylko wtedy, gdy k jest dzielnikiem tej liczby. Z tego powodu zwykle ogranicza się rozważania do nieujemnych dzielników k nieujemnych liczb m. Jeśli m > 0 i m = k p, gdzie k, p Z, to k = m/p, zatem k = m/ p m. Zatem wszystkie dzielniki liczby m leżą w przedziale między m i m. Definicja. Wspólnym dzielnikiem liczb m i n jest taka liczba całkowita, która jest dzielnikiem zarówno m, jak i n. Jest oczywistym, iż liczby i są zawsze wspólnymi dzielnikami m i n. Definicja. Jeśli liczby m i n nie są obie równe 0, to mogą mieć tylko skończoną liczbę wspólnych dzielników. Wtedy największy z nich nazywamy największym wspólnym dzielnikiem liczb m i n i oznaczamy go symbolem NWD(m,n). Przykład. Przykładowo wspólnymi dzielnikami liczb 8 i są liczby,, 4, a największym z nich jest 4. zatem NWD(8,) = 4. Przykładowo dzielnikami liczby są liczby, 3, 7,, zaś liczby są liczby,, 4, 8. zatem NWD(, ) =.
lgorytm NWD algorytm Euklidesa. Dopóki m n należy wykonywać:.a Jeśli m > n, należy podstawić m := m n;.b w przeciwnym wypadku, należy podstawić n := n m.. Jest zwracana liczba m. Przykład. Niech będą liczby m = 45 i n =. Kolejne etapy: (45,), (33,), (,), (9,), (9,3), (6,3), (3,3) Stąd NWD(45,) = 3. Szybki algorytm wyznaczania NWD(m, n).. W przypadku, gdy m n 0, należy wykonywać poniższe kroki:.a Jeśli m > n, podstaw m := m mod n;.b w przeciwnym wypadku podstaw n := n mod m.. Zwraca jest liczba max{n, m}. Przykład. Niech będą liczby m = 45 i n =. Kolejne etapy: (45,) (45 MOD, ) = (9,), (9, MOD 9) = (9,3), (9 MOD 3,3) = (0,3) Stąd max{0,3} = NWD(45,) = 3.
Sortowanie przez scalanie Innym przykładem algorytmu rekurencyjnego może być algorytm sortowania ciągu liczb (znaków). Dla uproszczenia będziemy zakładać, że długość ciągu jest potęgą dwójki. lgorytm sortowania przez scalanie merge-sort((a)).. Jeśli ciąg (a) ma tylko jeden element, zwróć ten ciąg.. W przeciwnym razie, należy wykonać następujące czynności:.a. podziel ciąg (a) na połowy (a) i (a);.b. zastosować algorytm sortowania do podciągu (a), tj. merge-sort(a);.c. zastosować algorytm sortowania do podciągu (a), tj. merge-sort(a);.d. połączyć ciągi (a) i (a) w jeden ciąg (a*) z zachowaniem kolejności i w ten sposób jako wynik jest zwracany jest ciąg (a*). Uwaga. Krok.d nazywany jest scalaniem i jego przebieg jest następujący. Na początku ciąg wynikowy jest pusty i ustawiamy po jednym wskaźniku na początku każdego ze scalanych ciągów (a) i (a). Z kolei porównujemy wskazywane elementy (do momentu, aż ich zabraknie), przy czym mniejszy z porównanych elementów przepisujemy na ciąg wynikowy i przesuwamy wskaźnik w tym ciągu, z którego został pobrany element do ciągu wynikowego. Przykład. Należy wykonać krok.d. powyższego algorytmu, tj. scalić następujące ciągi liczb: (,5,9,,5,4) i (,3,4,7,4,). Rozwiązanie. ktualne pozycje wskaźników oznaczone są przez pogrubienie czcionki. (,5,9,,5,5) (,3,4,7,4,) = [] (,5,9,,5,5) (,3,4,7,4,) = [] (,5,9,,5,5) (,3,4,7,4,) = [,] (,5,9,,5,5) (,3,4,7,4,) = [,,3] (,5,9,,5,5) (,3,4,7,4,) = [,,3,4] (,5,9,,5,5) (,3,4,7,4,) = [,,3,4,5] (,5,9,,5,5) (,3,4,7,4,) = [,,3,4,5,7] (,5,9,,5,5) (,3,4,7,4,) = [,,3,4,5,7,9] (,5,9,,5,5) (,3,4,7,4,) = [,,3,4,5,7,0,] (,5,9,,5,5) (,3,4,7,4,) = [,,3,4,5,7,0,,4] (,5,9,,5,5) (,3,4,7,4,) = [,,3,4,5,7,0,,4,5] (,5,9,,5,5) (,3,4,7,4,) = [,,3,4,5,7,0,,4,5,] (,5,9,,5,5) (,3,4,7,4,) = [,,3,4,5,7,0,,4,5,,5] Ciąg liczb (znaków) można posortować za pomocą metody przez scalanie merge-sort używając tzw. drzewo rekursji powstające podczas obliczeń.
D r z e w o s c a l a n i a D r z e w o r e k u r s j i Rekurencja dr hab. prof. nadzw. Tadeusz ntczak Przykład. Przy użyciu metody przez scalanie merge-sort posortować ciąg 5, 8, 3,,, 4,, 7. Narysować drzewo rekursji powstające podczas wykonywania obliczeń podczas stosowania tego algorytmu. 5, 8, 3,, 4,, 9, 7 5, 8, 3, 4,, 9, 7 5, 8 3, 4, 9, 7 5 8 3 4 9 7 5, 8, 3, 4 7, 9, 3, 5, 8,, 4, 7, 9,, 3, 4, 5, 7, 8, 9 Inny przykładem wykorzystania drzewa rekursji jest przekładanie krążków różnej wielkości z jednego palika na drugi przy wykorzystaniu trzeciego palika. Przykład. Przypuśćmy, że mamy trzy paliki, B i C. Na paliku znajduje się n krążków rożnej wielkości, osadzonych w porządku od największego na dole do najmniejszego na górze. Paliki B i C są początkowo puste. Należy przenieść wszystkie krążki z palika na palik C, posługując się w razie potrzeby palikiem B, przy czym: (i) można przenosić tylko po jednym krążku; (ii) nie można umieszczać krążka większego na mniejszym. lgorytm Przełoż(n,,B,C): przekładanie n krążków z palika na C przy wykorzystaniu palika B.. Jeśli n =, to należy przełożyć krążek z na C.. W przeciwnym przypadku, należy wykonać następujące czynności:.a. należy zastosować algorytm do n krążków, tj. przełóż(n,,b,c); tzn. przekładanie odbywa się z palika na palik B;.b. należy przełożyć n-ty krążek z na C;.c. należy zastosować algorytm do n krążków, tj. przełóż (n,b,c,), tzn. przekładanie odbywa się z palika B na palik C
n Przykład. Zakładając, że wierzchołek o etykiecie odpowiada wywołaniu procedury przełóż(n,,b,c), należy narysować drzewo rekursji dla B,C przekładania czterech krążków z palika na B. Następnie należy wypisać ciąg przełożeń. 4 C,B 3 B,C B 3 C, C,B C B, B, C C, B B,C B C, C, B B, C B C, C, B B, C B C, Sposób przekładania krążków jest wyznaczony poprzez przeszukanie powyższego drzewa w tzw. porządku inorder, wypisując za każdym razem, kiedy odwiedzamy węzeł, wykonanie odpowiedniego przełożenia krążka n w kroku.b: #n: B. #: B; #: C; #: B C; #3: B; #: C ; #: C B; #: B; #4: C; #: B C; #: B ; #: C ; #3: B C; #: B; #: C; #: B C;