Musterlösungen 4. Übung

Werbung
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
Herunterladen