Musterlösungen 5. Übung

Werbung
1 - Korrektur
2 - Abstrakte Datentypen für arithmetische Ausdrücke
Der Datentyp
Wir beginnen zunächst mit dem algebraischen Datentyp für Ausdrücke. Hierfür definieren wir einen Konstruktor
Number für Zahlen, sowie je einen Konstruktor für die 4 Operationen:
data Expr
= Const Int
| Add Expr Expr
| Sub Expr Expr
| Mul Expr Expr
| Div Expr Expr
deriving Show
Eine konkreten Ausdruck wie beispielsweise (3 + 4) ∗ ((73 − 37)/6) können wir dann wie folgt definieren:
expr :: Expr
expr = Mul (Add (Const 3) (Const 4)) (Div (Sub (Const 73) (Const 37)) (Const 6))
Das sieht jedoch relativ kompliziert aus (Scheme!?). Dies können wir lösen, indem wir nicht wie oben Präfixkonstruktoren, sondern stattdessen Infixkonstruktoren verwenden. Damit diese als solche erkannt werden
können, müssen Infixkonstruktoren mit einem Doppelpunkt : beginnen und weitere Symbole enthalten.
1
2
3
4
5
6
7
data Exp
= Number Int
| Exp :+: Exp
| Exp :-: Exp
| Exp :*: Exp
| Exp :/: Exp
deriving Show
Nun können wir den Ausdruck wie folgt notieren:
8
9
test :: Exp
test = (Number 3 :+: Number 4) :*: ((Number 73 :-: Number 37) :/: Number 6)
Wir können optional auch noch Präzedenzen (Bindungsstärken) für die Konstruktoren festlegen um Klammern
zu sparen:
10
11
infixl 6 :+:, :-:
infixl 7 :*:, :/:
Dies definiert die 4 Konstruktoren wegen infixl als links-assoziativ, im Gegensatz zu infixr (rechtsassoziativ)
und infix (nicht assoziativ), und die multiplikativen Operationen binden stärker.
Damit kann der Ausdruck 1 + 2 ∗ 3 als
12
13
seven :: Exp
seven = Number 1 :+: Number 2 :*: Number 3
notiert werden.
Auswertung
Da Exp eine induktiv definierte Datenstruktur ist, versuchen wir zunächst für jeden Fall eine Gleichung
anzugeben, und erhalten direkt die folgende Lösung:
1
14
15
16
17
18
19
eval
eval
eval
eval
eval
eval
:: Exp -> Int
(Number i) =
(e1 :+: e2) =
(e1 :-: e2) =
(e1 :*: e2) =
(e1 :/: e2) =
i
eval
eval
eval
eval
e1
e1
e1
e1
+ eval e2
- eval e2
* eval e2
`div` eval e2
Binärbäume
Arithmetische Ausdrücke lassen sich gut als Termbäume darstellen, wobei an den Blättern Zahlen und an
den Knoten die Operationen stehen. Wenn wir zunächst die 4 Konstruktoren für Operationen durch einen
gemeinsamen Fall abdecken und anschließend die Beschriftungen (Zahlen und Operatornamen) durch Typvariablen ersetzen, erhalten wir schlussendlich eine Datenstruktur für Binärbäume mit prinzipiell unterschiedlichen
Beschriftungen an den Blättern und den Knoten:
1. Gemeinsame Teile herausfaktorisieren:
data Exp
= Number Int
| Bin Op Exp Exp
data Op = Add | Sub | Mul | Div
2. Typen und Konstruktoren umbenennen:
data Tree
= Leaf Int
| Node Op Tree Tree
data Op = Add | Sub | Mul | Div
3. Beschriftungen verallgemeinern:
data Tree a b
= Leaf a
| Node b (Tree a b) (Tree a b)
data Op = Add | Sub | Mul | Div
Somit lassen sich arithmetische Ausdrücke wie folgt darstellen:
20
21
data Tree a b = Leaf a | Node b (Tree a b) (Tree a b)
deriving Show
22
23
24
data Op = Add | Sub | Mul | Div
deriving Show
25
26
type Exp' = Tree Int Op
Damit wir konkrete Ausdrücke einfacher konstruieren können, definieren wir uns die folgenden
Hilfs(konstruktor)funktionen, auch Smartkonstruktoren genannt:
27
28
number :: Int -> Exp'
number = Leaf
29
30
31
(.+.) :: Exp' -> Exp' -> Exp'
(.+.) = Node Add
32
33
34
(.-.) :: Exp' -> Exp' -> Exp'
(.-.) = Node Sub
35
36
37
(.*.) :: Exp' -> Exp' -> Exp'
(.*.) = Node Mul
38
2
39
40
(./.) :: Exp' -> Exp' -> Exp'
(./.) = Node Div
Somit könnte unser Ausdruck test von oben wie folgt in der Baumstruktur definiert werden:
41
42
test' :: Exp'
test' = (number 3 .+. number 4) .*. ((number 73 .-. number 37) ./. number 6)
Auch für diese Funktionen könnte man wie oben noch Präzedenzen festlegen.
Auswertung
Die Auswertungsfunktion ergibt sich wie oben, allerdings müssen wir noch einem Wert des Typs Op die
dazugehörige Funktion als Bedeutung (Semantik) zuordnen. Wir nennen diese Funktion sem:
43
44
45
46
47
sem
sem
sem
sem
sem
:: Op
Add =
Sub =
Mul =
Div =
-> Int -> Int -> Int
(+)
(-)
(*)
div
Nun können wir die Auswertung wie folgt definieren:
48
49
50
eval' :: Exp' -> Int
eval' (Leaf
n) = n
eval' (Node op l r) = sem op (eval' l) (eval' r)
Darstellung
Üblicherweise möchte man Ausdrücke aber nicht nur ausrechnen, sondern vielleicht auch noch schön formatiert
ausgeben. Wir definieren uns hierzu eine Funktion pshow (kurz für “pretty show”):
51
52
53
pshow :: Exp' -> String
pshow (Leaf
n) = show n
pshow (Node op l r) = concat ["(", pshow l, showOp op, pshow r, ")"]
54
55
56
57
58
59
showOp
showOp
showOp
showOp
showOp
:: Op
Add =
Sub =
Mul =
Div =
-> String
"+"
"-"
"*"
"/"
Faltung
Es fällt auf, dass die Funktionen eval' und pshow insofern gleichartig vorgehen, als dass für Blätter die jeweilige
Beschriftung verarbeitet wird, während bei Knoten die Teilbäume zunächst rekursiv verarbeitet und dann mit
der Knotenbeschriftung kombiniert werden. Dies entspricht genau dem Konzept einer Faltung auf Binärbäumen:
60
61
62
foldTree :: (a -> c) -> (b -> c -> c -> c) -> Tree a b -> c
foldTree f _ (Leaf
x) = f x
foldTree f g (Node y l r) = g y (foldTree f g l) (foldTree f g r)
Somiz können wir beide Funktionen auch mittels foldTree implementieren:
63
64
eval2 :: Exp' -> Int
eval2 = foldTree id sem
65
66
67
pshow2 :: Exp' -> String
pshow2 = foldTree show (\op s1 s2 -> concat ["(", s1, showOp op, s2, ")"])
Beliebigstellige Operationen
Um Operationen mit beliebiger Stelligkeit darstellen zu können, reicht es aus die beiden Teilbäume eines
Knotens durch eine Liste von Teilbäumen zu ersetzen:
3
68
69
data NTree a b = NLeaf a | NNode b [NTree a b]
deriving Show
Diese Bäume werden in der Literatur übrigens oft als “rose trees”, zu deutsch “Rhododendron-Bäume”,
bezeichnet.
3 - Suchbäume
Wir beginnen mit dem Datentyp eines Suchbaums:
1
2
data SearchTree = Empty | Branch SearchTree Int SearchTree
deriving (Eq, Show)
Die insert-Funktion fügt ein Element nur ein wenn es noch nicht vorhanden ist. In dem Fall wird für das
Element ein neues Blatt erzeugt und so in den Baum gehängt, dass es rechts von kleineren und links von
größeren Elementen steht.
3
4
5
6
7
8
insert :: Int -> SearchTree -> SearchTree
insert x Empty
= Branch Empty x Empty
insert x (Branch l n r)
| x == n
= Branch l n r
| x < n
= Branch (insert x l) n r
| otherwise = Branch l n (insert x r)
Die Funktion isElem nutzt die Ordnung des Baums, um die richtige Beschriftung zu finden:
9
10
11
12
13
14
isElem :: Int -> SearchTree -> Bool
isElem _ Empty
= False
isElem x (Branch l n r)
| x == n
= True
| x < n
= isElem x l
| otherwise = isElem x r
Zum Löschen einer Beschriftung gibt es unterschiedliche Strategien. Knoten mit weniger als zwei nicht-leeren
Teilbäumen können einfach gelöscht werden, aber andere Knoten nicht, da dann zwei nicht-leere Teilbäume zu
einem einzigen kombiniert werden müssen.
Eine einfache Möglichkeit ist, den linken Teilbaum ganz links im rechten einzufügen oder umgekehrt. Dadurch
wird die Höhe des Baumes aber unnötig groß. Besser ist es, eine geeignete Beschriftung aus einem der Teilbäume
zu löschen und diese an die frei gewordene Stelle zu schreiben. Man kann entweder die größte (rechteste)
Beschriftung des linken oder die kleinste (linkeste) des rechten Teilbaumes an diese Stelle schreiben.
Für letzteres benötigt man eine Funktion splitMin, die die kleinste Beschriftung eines sortierten Baumes und
den Restbaum als Ergebnis liefert. Da Funktionen nur ein Ergebnis liefern können, liefern wir ein Paar aus
beiden Teilen:
15
16
17
18
19
splitMin :: SearchTree -> (Int, SearchTree)
splitMin Empty
= error "splitMin: empty search tree"
splitMin (Branch Empty n r) = (n, r)
splitMin (Branch l
n r) = (m, Branch t n r)
where (m, t) = splitMin l
Die splitMin Funktion ist partiell, da sie nur für nicht-leere Bäume definiert ist. Sie wird aber auch nur auf
solchen aufgerufen.
Mit ihrer Hilfe können wir delete implementieren. Da jede Beschriftung höchstens einmal im Baum vorkommt,
genügt es, die erste gefundene zu entfernen.
20
21
delete :: Int -> SearchTree -> SearchTree
delete _ Empty
= Empty
4
22
23
24
25
26
27
28
29
30
31
32
33
34
delete x (Branch Empty n r)
| x == n
= r
| x < n
= Branch Empty n
| otherwise = Branch Empty n
delete x (Branch l n Empty)
| x == n
= l
| x < n
= Branch (delete
| otherwise = Branch l
delete x (Branch l n r)
| x == n
= Branch l
| x < n
= Branch (delete
| otherwise = Branch l
where (m, t) = splitMin r
r
(delete x r)
x l) n Empty
n Empty
m t
x l) n r
n (delete x r)
4 - Polymorphe binäre Blatt-Bäume
Der folgende Datentyp representiert Bäume mit Beschriftungen an Blättern.
1
2
data Tree a = Leaf a | Tree a :&: Tree a
deriving (Eq,Show)
Die flatTree-Funktion soll einen Baum von Bäumen zu einem einzigen Baum flach klopfen. Wir könnten sie
rekursiv wie folgt definieren:
3
4
5
flatTree' :: Tree (Tree a) -> Tree a
flatTree' (Leaf x) = x
flatTree' (s :&: t) = flatTree s :&: flatTree t
Die interessante Regel ist die zweite, die den Leaf-Konstruktor der äußeren Baumstruktur löscht.
Mit ein wenig Vorausschau können wir uns die Implementierung von flatTree erleichtern. Wählen wir beim
Typ von extendTree für die Typvariable a den konkreten Typ Tree b, dann ergibt sich genau der Typ von
flatTree. Aber nicht nur das, es ergibt sich auch die Implementierung:
6
7
flatTree :: Tree (Tree a) -> Tree a
flatTree = extendTree id
Auch die mapTree-Funktion können wir entweder direkt oder mit Hilfe einer anderen Funktion implementieren.
Hier die direkte Variante:
8
9
10
mapTree' :: (a -> b) -> Tree a -> Tree b
mapTree' f (Leaf x) = Leaf (f x)
mapTree' f (s :&: t) = mapTree f s :&: mapTree f t
Statdessen können wir auch wieder extendTree verwenden, indem wir die übergebene Funktion mit dem
Leaf{.haskell}-Konstruktor kombinieren:
11
12
mapTree :: (a -> b) -> Tree a -> Tree b
mapTree f = extendTree (Leaf . f)
Die Funktion extendTree können wir entweder direkt implementieren oder mit foldTree. Hier die direkte
Implementierung:
13
14
15
extendTree' :: (a -> Tree b) -> Tree a -> Tree b
extendTree' f (Leaf x) = f x
extendTree' f (s :&: t) = extendTree' f s :&: extendTree' f t
Und hier die Variante mit foldTree:
16
17
extendTree :: (a -> Tree b) -> Tree a -> Tree b
extendTree f = foldTree f (:&:)
5
Wenn wir die Implementierungen von flatTree und mapTree verwenden, die nicht auf extendTree basieren,
können wir extendTree auch mit deren Hilfe definieren:
18
19
extendTree'' :: (a -> Tree b) -> Tree a -> Tree b
extendTree'' f = flatTree' . mapTree' f
Die einzige Funktion, die wir direkt implementieren müssen, da wir sie nicht auf die anderen zurückführen
können, ist foldTree. Sie bekommt für jeden Konstruktor des Tree-Datentyps ein Argument um diesen zu
ersetzen:
20
21
22
foldTree :: (a -> b) -> (b -> b -> b) -> Tree a -> b
foldTree l _ (Leaf x) = l x
foldTree l f (s :&: t) = foldTree l f s `f` foldTree l f t
5 - Folds
•
•
•
•
foldr
foldl
foldr
foldl
(:)
(*)
(-)
(-)
[] ist die Identitätsfunktion auf Listen.
1 [x1...xn] berechnet ((1*x1)*x2)...*xn, sprich das Produkt der Elemente der Liste.
1 [x1...xn] berechnet x1-(x2-...(xn-1)).
1 [x1...xn] berechnet ((1-x1)-x2)...-xn bzw. 1 - sum [x1...xn].
Die map Funktion erhält die Listenstruktur und bildet nur die Elemente auf neue ab.
9
10
mapr, mapl :: (a -> b) -> [a] -> [b]
mapr f = foldr ((:) . f) []
Aber wie kommt man auf Ausdrücke wie (:) . f? Indem man mit einer anonymen Funktion anfängt und diese
systematisch umformt:
11
12
13
14
15
\x xs
\x
\x
\x
(:) .
->
->
->
->
f
f x : xs
(f x :)
(:) (f x)
((:) . f) x
Funktionen ohne explizite Argumente nennt man übrigens punktfrei.
Auch die map Funktion kann man mit foldl nur umständlich definieren, und das führt zur gleichen Verschlechterung der Laufzeit wie bei append:
16
mapl f = foldl (\xs x -> xs ++ [f x]) []
Die reverse-Funktion hingegen implementiert man am besten mit foldl. Interessanterweise entspricht die
Definition mit foldr genau der naiven (ineffizienten) Variante und die Definition mit foldl der effizienten
Implementierung:
17
18
19
20
reverser, reversel :: Eq a => [a] -> [a]
reverser = foldr (\x xs -> xs ++ [x]) [] -- quadratisch
reversel = foldl (\xs x -> x : xs)
-- linear
-- reversel = foldl (flip (:)) []
Die zweite Definition nutzt das zweite Argument von foldl als Akkumulator, in dem die gegebene Liste
umgedreht wird.
unzip lässt sich recht leicht mit foldr implementieren, aber schwierig mit foldl:
20
21
22
unzipr, unzipl :: [(a, b)] -> ([a], [b])
unzipr = foldr (\(a,b) (as,bs) -> (a : as, b : bs)) ([],[])
unzipl = foldl (\(as,bs) (a,b) -> (as ++ [a], bs ++ [b])) ([],[])
6
Die Variante mit foldr hat hier eine lineare Laufzeit bezogen auf die Listenlänge, die Variante mit foldl ist
quadratisch.
Bei nub verhält es sich ähnlich, wobei wegen des filter beide Implementierungen eine quadratische Laufzeit
besitzen.
23
24
25
nubr, nubl :: Eq a => [a] -> [a]
nubr ys = foldr (\x xs -> x : filter (x/=) xs) [] ys
nubl ys = foldl (\xs x -> filter (x/=) xs ++ [x]) [] ys
7
Herunterladen