POLITECHNIKA KRAKOWSKA WYDZIAŁ INŻYNIERII ELEKTRYCZNEJ i KOMPUTEROWEJ Katedra Automatyki i Technik Informacyjnych Algorytmy i Struktury Danych www.pk.edu.pl/~zk/aisd_hp.html Wykładowca: dr inż. Zbigniew Kokosiński zk@pk.edu.pl
Wykład 4: R e k u r s j a 1. Pojęcie rekursji. 2. Rekursja liniowa i drzewiasta. 3. Rekurencyjne wyznaczanie wartości ciągów: silnia, liczby Fibonacciego, współczynniki dwumienne 4. Inne algorytmy rekurencyjne: wieże Hanoi, sortowanie MergeSort i QuickSort trawersowanie drzew binarnych 5. Analiza zalet i wad rekursji. 6. Eliminacja rekursji: iteracja, zastosowanie stosu. 7. Rozwiązywanie rekurencji: metoda podstawiania, metoda iteracyjna, metoda rekurencji uniwersalnej.
Pojęcie rekursji Rekursja jest silnym narzędziem w definicjach matematycznych. Potęga rekursji uwidacznia się w możliwości definiowania nieskończonego zbioru obiektów (liczb naturalnych, drzew, krzywych Hilberta, krzywych Sierpińskiego) za pomocą skończonego wyrażenia. (Wirth 1980) W informatyce rekursja jest techniką programowania, w której procedura lub funkcja wywołuje samą siebie (przykład: wybór maksimum zbioru).
Rekursja liniowa Rekursyjną funkcją liniową nazywamy rekursję, która wykonuje tylko jedno wywołanie rekurencyjne samej siebie. Uwaga 1: nie wystarczy, że wywołanie występuje w jednym miejscu funkcji, bo może być np. wewnątrz pętli. Uwaga 2: wywołanie może następować w dwóch miejscach funkcji, np. w instrukcji warunkowej if else, a być wykonywane tylko jeden raz. Uwaga 3: drzewo rekursji dla rekursyjnej funkcji liniowej ma bardzo prostą postać łańcucha, w którym każdy wierzchołek posiada jednego potomka; przykładem jest tu funkcja obliczająca wartość silni.
Rekursja drzewiasta Rekursyjną funkcją drzewiastą nazywamy rekursję, która wykonuje więcej niż jedno wywołanie rekurencyjne samej siebie. Uwaga 1: najbardziej znane są fukcje z dwoma wywołaniami samych siebie (tzw. rekursja podwójna). Uwaga 2: podwójna rekursja jest czasem optymalna (z dokładnością do stałej); przykłady: trawersowanie drzewa w czasie zależnym od jego rozmiaru, rozwiązanie problemu wież Hanoi w czasie wykładniczym odpowiadającym naturze problemu. Uwaga 3: funkcja rekurencyjna może zostać zastąpiona przez równoważną jej z punktu widzenia złożoności funkcję iteracyjną (przy wykorzystaniu stosu i pewnego schematu konwersji); rozwiązania iteracyjne można tworzyć bezpośrednio bez definiowania rekursji i posługiwania się schematem.
Ciągi liczbowe definicje rekurencyjne
Silnia (ang. Factorial) Ciągi liczbowe: silnia n!=n (n-1) 2 1= n (n-1)! 1!=1 2!=2 3!=6 4!=24 5!=120 6!=720... Formuła Stirlinga : n! (n/e)^n (2πn)^(1/2) Program rekurencyjny: unsigned long silnia(int x) { if (x==0) return 1; else return x * silnia(x-1); }
Wykonanie silnia(3) rekursja liniowa unsigned long silnia(int x) { if (x==0) return 1; else return x * silnia(x-1); }
Ciągi liczbowe: liczby Fibonacciego Liczby Fibonacciego można obliczyć ze wzorów: F(1)=1, F(2)=1, F(n)=F(n-1)+F(n-2), dla n>2. Sekwencja liczb Fibonacciego to: 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 377, 610, 987, 1597, 2584, Dla dużych wartości n zachodzi F(n)/F(n-1) 1.618. Jest to tzw. złoty podział, stosowany jako miara klasycznych proporcji w architekturze. Program rekurencyjny: unsigned long f(int x) { if (x<2) return 1; else return (f(x-1) + f(x-2)); }
Ciągi liczbowe: współczynniki dwumienne Współczynnik dwumienny C(n,k) liczba wszystkich kombinacji k-elementowych zbioru n-elementowego: C(n,k)=0, C(n,k)=1, C(n,k)=C(n-1,k)+C(n-1,k-1), dla k >n; dla k=0 i dla k=n; dla 0<k<n. Liczby C(n,k) tworzą tzw. trójkąt Pascala. Program rekurencyjny: unsigned long C(int x, int y) { if (y>x) return 0; else if (y==0 (y==x) return 1; else if (y>0)&&(y<x) return (C(x-1,y) + C(x-1,y-1)); }
Wieże Hanoi 1
Wieże Hanoi 2
Sortowanie przez scalanie (MergeSort) 1
Sortowanie przez scalanie (MergeSort) 2
Sortowanie przez scalanie (MergeSort) 3
Sortowanie szybkie (QuickSort) 1
Sortowanie szybkie (QuickSort) 2
Sortowanie szybkie (QuickSort) 3
Sortowanie szybkie (QuickSort) 4
Sortowanie szybkie (QuickSort) 5
Trawersowanie drzew binarnych porządki: preorder, inorder oraz postorder
Zalety rekursji 1. Ogólna metoda rozwiązywania rozmaitych problemów. 2. Często jest to metoda wynikająca z rekurencyjnego zdefiniowania rozwiazania problemu ( dziel i zwyciężaj ). 3. Zapis algorytmu (programu rekurencyjnego) jest bardzo zwięzły. 4. Niejawne wykorzystywanie stosu jest bardzo wygodne dla programisty. 5. Istnieją metody wyznaczania złożoności obliczeniowej funkcji rekurencyjnych.
1. Metoda nie jest uniwersalna. Wady rekursji 2. Często nie jest łatwo znaleźć rozwiązanie rekurencyjne. 3. Rekursja drzewiasta wymaga często znacznej ilości pamięci na przechowanie stanu obliczeń w momencie każdego wywołania funkcji. 4. Rekursja drzewiasta wymaga często wielokrotnego obliczania tych samych wartości, które ulegają zatarciu, stąd wykładnicza złożoność czasowa tej metody. 5. Program rekurencyjny korzysta w sposób niejawny ze stosu, co może prowadzić do błędu wykonania programu polegającego na przepełnieniu stosu (stack overflow). 6. Niektóre języki programowania (FORTRAN) zabraniały stosowania rekursji. 7. Wyznaczenie złożoności obliczeniowej funkcji rekurencyjnej może być bardzo żmudne.
Analiza złożoności rekursji w strukturach drzewiastych liczby Fibonacciego Drzewo binarne pokazuje wykonanie algorytmu rekursyjnego. Obliczając funkcję f(5) otrzymujemy drzewo wywołań rekurencyjnych w algorytmie trawersowane w porządku preorder. Krawędzie skierowane w dół odpowiadają argumentowi funkcji f, a skierowane w górę zwróconej wartości funkcji. Obserwujemy wielokrotne wywołania z tym samym argumentem. Liczba liści drzewa odpowiada sumie liczby wywołań f(1) i f(2) z wartościami 1, czyli f(n). Stąd f(n)=1+1+...+1. Rekursja dekomponuje f(n) na funkcje zwracające 1 a następnie sumuje te jedynki wykonując f(n)-1 operacji +, z których każda jest wykonywana w odrębnym wywołaniu! Liczba wszystkich wywołań f jest równa 2f(n)-1.
Analiza złożoności rekursji w strukturach drzewiastych liczby Fibonacciego Twierdzenie Funkcja f(n) rośnie wykładniczo z n. Dowód. Zaobserwujmy, że f(n) = f(n-1) + f(n-2) = f(n-2)+f(n-3)+f(n-2) = 2f(n-2)+f(n-3) > > 2f(n-2). Stąd f(n) > 2f(n-2) > 2(2f(n-2-2) = 4f(n-4) > 4(2f(n-4-2) = 8f(n-6) > > (2^k) f(n-2k), dla n-2k>0. Jeśli n jest parzyste zatrzymujemy się dla n-2k=2; w przeciwnym wypadku - dla n-2k=1. W obu przypadkach k=(n-1)/2 (wynik dzielenia jest liczbą całkowitą zaokrągloną w dół). Ponieważ f(1)=f(2)=1, otrzymujemy : f(n) > 2^(n-1)/2 (po prawej stronie nierówności jest funkcja wykładnicza). c.b.d.o.
Analiza złożoności rekursji w strukturach drzewiastych współczynniki dwumienne Drzewo binarne pokazuje wykonanie algorytmu rekursyjnego. Obliczając funkcję C(4,2) otrzymujemy drzewo wywołań rekurencyjnych w algorytmie trawersowane w porządku preorder. Krawędzie skierowane w dół odpowiadają argumentowi funkcji C, a skierowane w górę zwróconej wartości funkcji. Obserwujemy wielokrotne wywołania z tym samym argumentem. Liczba liści drzewa odpowiada sumie liczby wywołań C z wartościami 1, czyli C(n,k). Stąd mamy C(n,k)=1+1+...+1. Rekursja dekomponuje C(n,k) na funkcje zwracające 1 a następnie sumuje te jedynki wykonując C(n)-1 operacji +, z których każda jest wykonywana w odrębnym wywołaniu! Liczba wszystkich wywołań C jest równa 2C(n)-1.
Eliminacja rekursji schemat iteracyjny
Eliminacja rekursji wykorzystanie stosu 1. Jednym ze sposobów eliminacji rekursji jest jawne wykorzystanie w programie struktury stosu do przechowywania stanu obliczeń. 2. Stos musi mieć odpowiedni rozmiar odpowiadający rozmiarowi problemu. Tu również może nastąpić błąd przepełnienia stosu. 3. Niklaus Wirth w swoje klasycznej ksiażce Algorytmy + struktury danych = programy podał nierekurencyjną wersję sortowania szybkiego z jawnym stosem (program 2.11).
Złożoność obliczeniowa algorytmów rekurencyjnych rozwiązywanie rekurencji
Metoda podstawiania
Metoda iteracyjna
Kontrukcja drzewa rekursji dla równania rekurencyjnego T(n)=2T(n/2)+n^2
Kontrukcja drzewa rekursji dla równania rekurencyjnego T(n)=T(n/3)+T(2n/3)+n
Kontrukcja drzewa rekursji dla równania rekurencyjnego T(n)=aT(n/b)+f(n)
Metoda rekurencji uniwersalnej 1
Metoda rekurencji uniwersalnej 2
Źródła wzorów, przykładów i rysunków : 1. Cormen T.H., Leiserson C.E., Rievest R.L. : Wprowadzenie do algorytmów, WNT 1999 2. Kubale M. : Introduction to computational complexity and algorithmic graph coloring, GTN 1998 3. Sedgewick R. : Algorithms in C, Addison-Wesley 1990 4. Stojmenovič I. : Recursive algorithms in computer science courses: Fibonacci numbers and binomial coefficients, IEEE Trans. Education 43 (3), 2000, 273-276. 5. Wirth N. : Algotytmy + struktury danych = programy, WNT 1980