1 - Fibonacci Hier noch einmal die Definitionen von fib1 und fib2: 1 2 3 4 5 6 fib1 :: Int -> Int fib1 n = if n == 0 then 0 else if n == 1 then 1 else fib1 (n - 1) + fib1 (n - 2) 7 8 9 10 11 12 13 fib2 :: Int -> Int fib2 n = fib2' 0 1 n where fib2' fibn fibnp1 n = if n == 0 then fibn else fib2' fibnp1 (fibn + fibnp1) (n - 1) Wichtig ist hierbei, dass der Typ des Ergebnisses auf Int festgelegt wurde. Dadurch wird für große Eingaben das Ergebnisses zwar inkorrekt (aufgrund eines Überlaufes, da der Zahlenbereich zu klein ist), die Laufzeit der arithmetischen Operationen ist aber auch unabhängig von der Zahlgröße. Wir verwenden den GHC 7.10.3 und tabellieren zunächst die Laufzeiten für fib1: Tabelle 1: Laufzeiten von fib1. n Laufzeit fib1 n [s] 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 0,22 0,36 0,57 1,02 1,49 2,41 3,95 6,33 10,23 16,36 26,58 43,30 70,08 112,28 181,41 Wir können nun die Laufzeiten von fib1 grafisch auftragen. Der Kurvenverlauf deutet eine exponentielle Laufzeit an, um sicher zu gehen verwenden wir nun eine logarithmische Skalierung: Wie erwartet erhalten wir nun einen geraden Verlauf, insgesamt hat fib1 also eine exponentielle Laufzeit. Da wir in jedem Aufruf von fib1 maximal 2 rekursive Aufrufe haben, können wir die Komplexität mit O(2n ) abschätzen, wobei n das initiale Argument ist. Für eine etwas exaktere Schätzung können wir auch die letzten beiden Laufzeiten dividieren: 181,41 112,28 ≈ 1, 616. Wir tabellieren nun die Laufzeiten für fib2: 1 Abbildung 1: Laufzeit von fib1 bei linearer Skalierung Abbildung 2: Laufzeit von fib1 bei logarithmischer Skalierung 2 Tabelle 2: Laufzeiten von fib2. n Laufzeit fib2 n [s] 1.000.000 2.000.000 3.000.000 4.000.000 5.000.000 6.000.000 7.000.000 8.000.000 9.000.000 10.000.000 11.000.000 12.000.000 13.000.000 14.000.000 15.000.000 0,90 1,82 2,71 3,60 4,49 5,29 6,30 7,21 8,14 9,06 9,94 10,85 11,69 12,34 13,17 Für fib2 erkennen wir die erwartete lineare Laufzeit: Abbildung 3: Laufzeit von fib2 bei linearer Skalierung Es ergibt sich hier eine Laufzeitkomplexität von O(n). 2 - ggT und kgV 1 2 3 ggT :: Int -> Int -> Int ggT a 0 = a ggT a b = ggT b (mod a b) 4 3 5 6 7 kgV :: Int -> Int -> Int -- kgV a b = (a * b) `div` ggT a b kgV a b = (a `div` ggT a b) * b 8 9 10 11 ggTL :: [Int] -> Int ggTL [] = 0 ggTL (x:xs) = ggT x (ggTL xs) 12 13 14 15 kgVL :: [Int] -> Int kgVL [] = 1 kgVL (x:xs) = kgV x (kgVL xs) 3 - Binärbäume Wir beginnen mit dem Datentyp Tree eines Binärbaums mit Beschriftungen an den Blättern: 1 2 data Tree a = Leaf a | Branch (Tree a) (Tree a) deriving (Eq, Show) Für einen Baum mit Zahlbeschriftungen kann man die Summe aller Knotenbeschriftungen rekursiv berechnen. 3 4 5 sumTree1 :: Tree Int -> Int sumTree1 (Leaf n) = n sumTree1 (Branch l r) = sumTree1 l + sumTree1 r Die Version mit Akkumulatortechnik hat dieselbe Laufzeit (linear in der Knotenanzahl). 6 7 8 9 10 sumTree2 :: Tree Int -> Int sumTree2 = sumTree 0 where sumTree k (Leaf n) = n + k sumTree k (Branch l r) = sumTree (sumTree k l) r Bäume kann man rekursiv spiegeln und dabei immer linke und rechte Teilbäume vertauschen. 11 12 13 mirrorTree :: Tree a -> Tree a mirrorTree l@(Leaf _) = l mirrorTree (Branch l r) = Branch (mirrorTree r) (mirrorTree l) Für die Aufzählung der Beschriftungen bietet sich die Aufzählung von links nach rechts an. 14 15 16 toList :: Tree a -> [a] toList (Leaf x) = [x] toList (Branch l r) = toList l ++ toList r Die Implementierung konkateniert die Elemente des linken Teilbaums mit der Liste der Elemente des rechten Teilbaums. Die Listenkonkatenation (++) hat dabei eine Laufzeit linear zur Länge der ersten Liste. Im schlechtesten Fall ist der Baum derart entartet, dass jeder rechte Teilbaum ein Blatt ist und der Baum nach links tief absteigt: Damit hat ein Baum mit n + 1 Beschriftungen auch eine Tiefe n. In Tiefe n existiert im linkem Teilbaum nur nur ein Element, sodass (++) für den linken Teilbaum eine Liste der Länge 1 durchläuft, in Tiefe n − 1 eine Liste der Länge 2 etc. Da die rechten Teilbäume Blätter sind ergibt sich hier jeweils eine konstante Laufzeit. Es ergeben sich also insgesamt 1 + 2 + . . . + n ∈ O(n2 ) Durchläufe, die Funktion hat also eine quadratische Laufzeit. 4 Abbildung 4: Entarteter Baum 5 4 - Listenfunktionen reversed Eine Funktion, die eine Liste umdreht, kann man rekursiv definieren, indem man das erste Element einer nicht-leeren Liste hinter die umgedrehte Restliste hängt: 1 2 3 reversed :: [a] -> [a] reversed [] = [] reversed (x:xs) = reversed xs ++ [x] Hier einige Beispiele: ghci> reversed [1,2,3] [3,2,1] ghci> reversed [1..10] [10,9,8,7,6,5,4,3,2,1] ghci> reversed [10,8..1] [2,4,6,8,10] Die Laufzeit von reversed ist quadratisch: Bei einer Liste der Länge n gibt es n rekursive Aufrufe von reversed. Jeder einzelne Aufruf benötigt so viele Schritte, wie die Funktion ++ benötigt um eine Liste der entsprechenden Länge abzuarbeiten. Die Laufzeit von ++ ist linear in der Länge des ersten Arguments, also ist die Laufzeit von reversed proportional zur Summe der Zahlen von 1 bis n, das heißt quadratisch in n, also reversed ∈ O(n2 ). Mit Hilfe der Akkumulatortechnik kann man eine Liste schneller umkehren: 4 5 6 7 reversedAcc reversedAcc where rev rev :: [a] -> l = rev l [] ys (x:xs) ys [a] [] = ys = rev xs (x:ys) Hierbei hat jeder einzelne der n Schritte konstante Laufzeit, die Gesamtlaufzeit ist also linear in der Länge n der gegebenen Liste. Zur Sicherheit ein paar Tests: ghci> reversedAcc [3,5,7] [7,5,3] ghci> reversedAcc [3,5..20] [19,17,15,13,11,9,7,5,3] ghci> reversedAcc [15,10..0] [0,5,10,15] indexOf Analog zur Funktion (!!) hat im Folgenden das erste Element einer Liste den Index 0. Die Funktion indexOf bekommt als erstes Argument das Element, dessen Index in der gegebenen Liste (zweites Argument) sie berechnen soll. Falls das Element nicht in der Liste enthalten ist, ist das Ergebnis Nothing. 8 9 10 11 12 13 indexOf :: Int -> [Int] -> Maybe Int indexOf _ [] = Nothing indexOf x (y:ys) | x == y = Just 0 | otherwise = case indexOf x ys of Nothing -> Nothing Just i -> Just (i + 1) Wir können das Ergebnis auch mit Hilfe eines Zählers in einer Hilfsvariablen berechnen: 6 13 14 15 16 indexOf' :: Int -> [Int] -> Maybe Int indexOf' = index 0 where index _ _ [] = Nothing index i x (y:ys) = if x == y then Just i else index (i + 1) x ys Beide Funktionen haben dieselbe Laufzeit und liefern dieselben Ergebnisse: ghci> indexOf 3 [] Nothing ghci> indexOf 3 [1,2,3] Just 2 ghci> indexOf 10 [1..9] Nothing ghci> indexOf 5 [1,3,5,7,5,3,1] Just 2 ghci> indexOf' 3 [] Nothing ghci> indexOf' 3 [1,2,3] Just 2 ghci> indexOf' 10 [1..9] Nothing ghci> indexOf' 5 [1,3,5,7,5,3,1] Just 2 inits und tails Die Funktionen inits und tails berechnen alle Anfangs- bzw. Endstücke einer Liste. Hier einige Tests: ghci> inits [] [[]] ghci> inits [1] [[],[1]] ghci> inits [1,2,3] [[],[1],[1,2],[1,2,3]] ghci> tails [] [[]] ghci> tails [1] [[1],[]] ghci> tails [1,2,3] [[1,2,3],[2,3],[3],[]] Man beachte, dass die leere Liste ein Anfangs- und Endstück jeder Liste, auch der leeren Liste, ist. Beide Funktionen kann man direkt rekursiv definieren. 21 22 23 inits :: [a] -> [[a]] inits [] = [[]] inits (x:xs) = [] : map (x:) (inits xs) 24 25 26 27 tails :: [a] -> [[a]] tails [] = [[]] tails (x:xs) = (x:xs) : tails xs insert Eine Funktion zum Einfügen eines Elementes an einer beliebigen Position in einer Liste kann man rekursiv definieren. Das Ergebnis von insert ist eine Liste aller möglichen Listen, die nach dem Einfügen entstehen können. 7 33 insert :: a -> [a] -> [[a]] In die leere Liste kann man ein Element nur auf eine Art einfügen: 34 insert x [] = [[x]] In eine nicht-leere Liste kann man ein Element entweder vorne oder rekursiv weiter hinten einfügen. Nach dem rekursiven Einfügen muss man noch das ursprüngliche erste Element vorne an jedes mögliche Ergebnis anfügen. 35 insert x (y:ys) = (x:y:ys) : map (y:) (insert x ys) Hier zwei Tests: ghci> insert 42 [] [[42]] ghci> insert 1 [2,3] [[1,2,3],[2,1,3],[2,3,1]] perms Die insert Funktion kann man verwenden um alle Permutationen einer Liste zu berechnen. Dazu fügt man das erste Element einer nicht-leeren Liste an einer beliebigen Stelle in jede Permutation der Restliste ein. 36 37 38 perms :: [a] -> [[a]] perms [] = [[]] perms (x:xs) = concatMap (insert x) (perms xs) Ein paar Tests: ghci> perms [] [[]] ghci> perms [1,2,3] [[1,2,3],[2,1,3],[2,3,1],[1,3,2],[3,1,2],[3,2,1]] ghci> length (perms [1..10]) == product [1..10] True 8