REKURENCJA W JĘZYKU HASKELL
Transkrypt
REKURENCJA W JĘZYKU HASKELL
REKURENCJA W JĘZYKU HASKELL Autor: Walczak Michał CZYM JEST REKURENCJA? Rekurencja zwana rekursją, polega na wywołaniu przez funkcję samej siebie. Algorytmy rekurencyjne zastępują w pewnym sensie iteracje. Zazwyczaj zadania rozwiązywane tą techniką są wolniejsze od iteracyjnego odpowiednika, natomiast rozwiązanie niektórych problemów jest znacznie wygodniejsze. Rekurencja jest często stosowana w matematyce. W języku Haskell nie istnieją takie instrukcje jak pętle, jest to związane z tym że nie jest możliwa zmiana wartości zmiennej. Implementacja powtarzających się czynności musiała zostać rozwiązana przez rekurencję. Przykład zastosowania rekurencji Ciąg Fibonacci'ego Jest to ciąg, gdzie każdy kolejny wyraz ciągu jest sumą dwóch poprzednich. Dwa pierwsze elementy ciągu musimy wyznaczyć bez rekurencji. F0=0 F1=1 Fn=Fn-1+Fn-2, dla n≥ 2 Początkowe wartości to: 0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, … Funkcja Maximum Implementacja w języku imperatywnym Najprawdopodobniej stworzylibyśmy zmienną, która przechowywałaby największą wartości oraz pętlę, która przy wykonywaniu sprawdzałaby kolejne elementy listy. Jeśli sprawdzany aktualnie element listy jest większy od aktualnej maksymalnej wartości, wtedy zapisalibyśmy ten element do naszej zmiennej. Kiedy pętla przestanie się wykonywać ostatni wynik jest naszym rozwiązaniem! Implementacja z użyciem rekurencji Funkcja dzieli listę na head i tail, aż do momentu otrzymania listy jednoelementowej i porównuje head, z maximum tail (czyli wywołuje funkcje maximum dla tail). Implementacja w języku Haskell Pierwszy warunek końcowy zwraca błąd jeśli nasza lista jest pusta. W wypadku jeśli jest to lista jednoelementowa - wynikiem będzie ten jeden element. W trzecim warunku końcowym rozłączamy naszą listę na głowę i ogon. Używamy funkcji where w celu zdefiniowania funkcji maxTail, jako maximum reszty listy, a następnie sprawdzamy warunek (if) czy głowa jest większa od reszty listy. Jeśli tak- zwracamy głowę jako wynik. W innym wypadku zwracamy maximum reszty listy. Przykład z użyciem funkcji maximum Weźmy tablicę liczb całkowitych [2,1,5]. Krok 1: Dzielimy tę tablicę na head(2) i tail(1,5) i wywołujemy funkcję maximum dla tail Przykład z użyciem funkcji maximum Weźmy tablicę liczb całkowitych [2,1,5]. Krok 1: Dzielimy tę tablicę na head(2) i tail(1,5) i wywołujemy funkcję maximum dla tail Krok 2: Tail jest ponownie dzielony na head(1) i tail(5) w wyniku tego podziału mamy spełniony warunek końca rekurencji. Przykład z użyciem funkcji maximum Weźmy tablicę liczb całkowitych [2,1,5]. Krok 1: Dzielimy tę tablicę na head(2) i tail(1,5) i wywołujemy funkcję maximum dla tail Krok 2: Tail jest ponownie dzielony na head(1) i tail(5) w wyniku tego podziału mamy spełniony warunek końca rekurencji. Krok 3: Porównujemy 1 i 5, funkcja zwraca nam 5. Przykład z użyciem funkcji maximum Weźmy tablicę liczb całkowitych [2,1,5]. Krok 1: Dzielimy tę tablicę na head(2) i tail(1,5) i wywołujemy funkcję maximum dla tail Krok 2: Tail jest ponownie dzielony na head(1) i tail(5) w wyniku tego podziału mamy spełniony warunek końca rekurencji. Krok 3: Porównujemy 1 i 5, funkcja zwraca nam 5. Krok 4: Porównujemy wartość zwróconą w kroku 3 czyli 5 z head czyli 2. 5 jest większe od 2 zatem funkcja zwraca 5 jako największą wartość tablicy. Czystszy zapis z użyciem funkcji max Funkcja replicate Funkcja replikacja pobiera dwa argument z czego pierwszy to int który mówi o tym ile razy ma zostać powtórzony drugi argument funkcji. Wynikiem działania tej funkcji jest lista powtarzających się elementów. Przykładowe wywołanie funkcji: replicate 3 5 zwraca nam listę [5,5,5] Funkcja Take Funkcja take jest to funkcja, która pobiera zadaną liczbę elementów z podanej tablicy. Przykładowe użycie funkcji take 3 [2, 1, 25, 3, 0, 5] Wynik [2,1,25] W tym przypadku wykorzystujemy n oraz od razu rozbijamy listę na znany nam już ogon i głowę. Bierzemy pierwszy element listy i łączymy go poprzez rekurencję (n-1) razy z pozostałymi elementami. Funkcja reverse Jest to funkcja która zwraca odwróconą listę do listy podanej a argumencie funkcji Przykładowe użycie funkcji reverse [2,1,4] Wynik to [4,1,2] Funkcja QuickSort Załóżmy, że mamy listę elementów typu Ord do posortowania. Najefektywniej można to osiągnąć za pomocą popularnego algorytmu QuickSort. W językach imperatywnych taki algorytm zajmować może nawet kilkanaście linijek kodu, a jego implementacja w Haskellu jest dużo krótsza i bardziej przejrzysta. Posortowaną listą nazywamy taką listę, której wszystkie elementy są mniejsze bądź równe największemu elementowi. Następnie zaczynając od pierwszej liczby sprawdzamy czy każda następna jest większa od poprzedniej. Implementacja algorytmu QuickSort Warunek końca określa sytuację, kiedy do posortowania zostaje pusta lista (co jest równoznaczne z tym, że jest już posortowana). Elementy listy, które są mniejsze lub równe headowi są sortowane i umieszczane na początku listy, następnie wstawiany jest head, a po nim posortowane elementy większe lub równe headowi. Podsumowanie Wykonaliśmy już trochę działań za pomocą rekurencji i powinniśmy zauważać już pewną zależność. Zazwyczaj definiujemy warunek końcowy, po czym definiujemy funkcję, która wykonuje operacje pomiędzy niektórymi elementami względem reszty. Nie ma znaczenia czy to jest drzewo, lista, czy inna struktura danych. Zazwyczaj warunkiem końcowym jest punkt, gdzie rekurencja traci sens (otrzymujemy ten sam wynik cały czas). W przypadku list, najczęściej warunkiem końcowym jest pusta lista. W przypadku drzew jest to punkt bez żadnych dzieci. Żeby myśleć rekurencyjne za warunek końcowy musimy uznać moment, w którym rekurencja nie zostaje użyta. Musimy także pamiętać o danych, z których będziemy korzystać, rozłożeniu parametrów funkcji oraz momencie, w którym użyjemy rekurencji.