Linearisierung Breitendurchlauf Arithmetische Ausdrücke Programmierung und Modellierung mit Haskell Algorithmen für Bäume Martin Hofmann Steffen Jost LFE Theoretische Informatik, Institut für Informatik, Ludwig-Maximilians Universität, München 19. Mai 2014 Martin Hofmann, Steffen Jost Programmierung und Modellierung 08-1 Linearisierung Breitendurchlauf Arithmetische Ausdrücke Binärbäume Ist A eine Menge, so wird die Menge A4 der Binärbäume mit Knotenmarkierungen in A (oder einfach Binärbäume über A) induktiv definiert durch: 1 2 Der leere Baum ε ist in A4 . Sind l und r in A4 und x in A, so ist das Tripel (l , x, r ) in A4 . Alternativ kann man die Mengen A4 n der Binärbäume mit Höhe (Tiefe) kleiner n rekursiv definieren durch: A4 0 = { } (kein Baum hat negative Höhe). 4 2 An+1 = {ε} ∪ {(l , x, r ) | x ∈ A, l , r ∈ A4 n }. 1 4 Man beweist leicht A4 n ⊆ An+1 (Kumulativität). S 4 4 A4 = n∈N A4 n ist der Limes der Folge A0 , A1 , . . . Die Höhe (Tiefe) eines Baums t ist das kleinste n, so dass t ∈ A4 n+1 . Martin Hofmann, Steffen Jost Programmierung und Modellierung 08-2 Linearisierung Breitendurchlauf Arithmetische Ausdrücke Terminologie für Bäume x heißt Wurzel oder (Wurzel)beschriftung von (x, l , r ). l heißt linker und r rechter Unterbaum von (x, l , r ). Ein Binärbaum der Form (x, ε, ε) heißt Blatt. ε heißt leerer Baum, jeder andere Baum ist nichtleer. Die Knoten(markierungen) und Teilbäume eines Binärbaums sind rekursiv definiert: Der leere Baum ε hat keine Knoten und nur sich selbst als Teilbaum. 2 Die Knoten von (x, l , r ) sind x plus die Knoten von l und r . Die Teilbäume von (x, l , r ) sind (x, l , r ) plus die Teilbäume von l und r . 1 Sei [x] definiert als Abkürzung für ein Blatt (x, ε, ε) und t = (6, (3, [2], (8, [5], ε)), (8, ε, [4])) t hat Knoten {2, 3, 5, 8, 6, 4} und Teilbäume {t, ((3, [2], (8, [5]ε)), (8, ε, [4])), (8, [5], ε), [2], [4], [5], ε}. Martin Hofmann, Steffen Jost Programmierung und Modellierung 08-3 Linearisierung Breitendurchlauf Arithmetische Ausdrücke Typische Darstellung eines Binärbaumes Wurzel: 6 Blätter: 2, 5, 4 6 3 8 2 8 4 5 Martin Hofmann, Steffen Jost Programmierung und Modellierung 08-4 Linearisierung Breitendurchlauf Arithmetische Ausdrücke Binärbäume in Haskell (Wdh.) data Tree a = Empty | Node a (Tree a) (Tree a) leaf :: a -> Tree a leaf a = Node a Empty Empty oder besser mit Record Syntax: data Tree a = Empty | Node { label :: a, left :: Tree a, right :: Tree a t = Node 6 (Node 3 (leaf 2) (Node 8 (leaf 5) Empty )) (Node 8 Empty (leaf 4)) Martin Hofmann, Steffen Jost Programmierung und Modellierung 08-5 Linearisierung Breitendurchlauf Arithmetische Ausdrücke Rekursion auf Bäumen Höhe eines Baumes: height :: Tree a -> Integer height ( Empty) = 0 height (Node _ l r) = 1 + max ( height l)( height r) Eine Funktion f auf alle Beschriftungen anwenden: mapTree :: (a -> b) -> Tree a -> Tree b mapTree f ( Empty ) = Empty mapTree f (Node x l r) = let l' = mapTree f l x' = f x r' = mapTree f r in Node x' l' r' Beachte: mapTree f t erzeugt einen komplett neuen Baum! mapTree (\x -> x) t liefert eine Kopie von t. Bringt aber nichts! Martin Hofmann, Steffen Jost Programmierung und Modellierung 08-6 Linearisierung Breitendurchlauf Arithmetische Ausdrücke Linearisierung Mit Hilfe eines Baumdurchlaufs (engl. tree traversal) können wir alle Knotenmarkierungen in einer Liste aufsammeln. Die Reihenfolge der Listenelemente hängt von der Art des Durchlaufs ab: 1 2 3 Vorordnung (engl. preorder) Symmetrische Ordnung (engl. inorder) Nachordnung (engl. postorder) Martin Hofmann, Steffen Jost Programmierung und Modellierung 08-7 Linearisierung Breitendurchlauf Arithmetische Ausdrücke Vorordnung Zuerst Markierung, dann linker, dann rechter Teilbaum. preorder :: Tree a -> [a] preorder ( Empty ) = [] preorder (Node x l r)) = [x] ++ preorder l ++ preorder r 1 2 6 3 4 7 5 Martin Hofmann, Steffen Jost Programmierung und Modellierung 08-8 Linearisierung Breitendurchlauf Arithmetische Ausdrücke Symmetrische Ordnung Zuerst linker Teilbaum, dann Markierung, dann rechter Teilbaum. inorder :: Tree a -> [a] inorder ( Empty ) = [] inorder (Node x l r) = inorder l ++ [x] ++ inorder r 5 2 7 1 4 6 3 Martin Hofmann, Steffen Jost Programmierung und Modellierung 08-9 Linearisierung Breitendurchlauf Arithmetische Ausdrücke Nachordnung Zuerst linker, dann rechter Teilbaum, dann Markierung. postorder :: Tree a -> [a] postorder ( Empty ) = [] postorder (Node x l r) = postorder l ++ postorder r ++ [x] 7 4 6 1 3 5 2 Martin Hofmann, Steffen Jost Programmierung und Modellierung 08-10 Linearisierung Breitendurchlauf Arithmetische Ausdrücke Generischer Tiefendurchlauf Alle Durchläufe bearbeiten die Teilbäume unabhängig voneinander. Das zugrundeliegende Schema heißt Tiefendurchlauf (engl. depth-first) foldTree :: ((b,a,b) -> b) -> b -> Tree a -> b foldTree fn fe Empty = fe foldTree fn fe (Node x l r) = fn( foldTree fn fe l,x, foldTree fn fe r) Die drei Baumlinearisierungen sind Instanzen davon: preorder t = foldTree (\(l,x,r) -> [x]++l++r) [] t inorder t = foldTree (\(l,x,r) -> l++[x]++r) [] t postorder t = foldTree (\(l,x,r) -> l++r++[x]) [] t Martin Hofmann, Steffen Jost Programmierung und Modellierung 08-11 Linearisierung Breitendurchlauf Arithmetische Ausdrücke Breitendurchlauf Beim Breitendurchlauf (engl. breadth-first traversal) werden die Teilbäume nicht unabhängig behandelt. 1 2 3 4 5 6 7 Der Baum wird schichtenweise abgearbeitet: Hier also 1, 2, 3, 4, 5, 6, 7 Martin Hofmann, Steffen Jost Programmierung und Modellierung 08-12 Linearisierung Breitendurchlauf Arithmetische Ausdrücke Breitendurchlauf Verallgemeinerung: Breitendurchlauf eines Waldes (engl. forest), einer Liste von Bäumen. breadthForest :: [Tree a] -> [a] breadthForest [] = [] breadthForest ( Empty : frst) = breadthForest frst breadthForest (Node x l r) : frst) = x : breadthForest (frst ++[l,r]) Zuerst alle Wurzeln, dann alle Knoten der Tiefe 1, . . . . breadthForest erlaubt eine elegante rekursive Formulierung wie oben. Für einen Breitendurchlauf eines einzelnen Baums t ruft man breadthForest([t]) auf. Martin Hofmann, Steffen Jost Programmierung und Modellierung 08-13 Linearisierung Breitendurchlauf Arithmetische Ausdrücke breadthForest[ ]= 1 2 3 4 5 6 7 1 : breadthForest[ , 2 4 5 3 ]= 6 7 1 :: 2 :: breadthForest[ 3 , 4 , 6 Martin Hofmann, Steffen Jost 5 ] = ... 7 Programmierung und Modellierung 08-14 Linearisierung Breitendurchlauf Arithmetische Ausdrücke Arithmetische Ausdrücke als Bäume Beispiel: Ein Mathe-Nachhilfe-Programm für Grundschüler. Es druckt zufällig einen arithmetischen Ausdruck wie (5 + 3) ∗ 2, und vergleicht den Lösungsvorschlag mit dem Wert des Ausdrucks (hier: 16). Die Zahlen sollen einstellig sein und die Operationen + und ∗. Den Ausdruck können wir intern als Binärbaum repräsentieren. ∗ Richtig: + 5 Falsch: 2 3 Martin Hofmann, Steffen Jost + ∗ 5 3 Programmierung und Modellierung 2 08-15 Linearisierung Breitendurchlauf Arithmetische Ausdrücke Arithmetische Ausdrücke als Binärbäume Es gibt drei Arten von Knotenmarkierungen: Zahl, “+”, “*”. data Label = Const ( Integer ) | Plus | Times type Expr = Tree Label Beispiel: a1 = Node Times (Node Plus (leaf( Const 5)) (leaf( Const 3))) (leaf( Const 2)) Martin Hofmann, Steffen Jost Programmierung und Modellierung 08-16 Linearisierung Breitendurchlauf Arithmetische Ausdrücke Auswertung Den Wert des Ausdrucks berechnen wir rekursiv: eval eval eval eval eval :: Expr -> Integer ( Empty ) = 0 (Node( Const n) _ _) = n (Node Plus l r) = eval l + eval r (Node Times l r) = eval l * eval r Wir sagen, eval (dt. auswerten) interpretiert den Ausdruck. a1v = eval a1 16 Übung: Implementieren Sie eval als Anwendung von foldTree! Martin Hofmann, Steffen Jost Programmierung und Modellierung 08-17 Linearisierung Breitendurchlauf Arithmetische Ausdrücke Ausdrücke ausdrucken Eine andere Interpretation des Ausdrucks ist seine Repräsentation as Zeichenkette. exprToString :: Expr -> String exprToString Empty = "" exprToString (Node ( Const n) _ _) = show n exprToString (Node Plus l r) = "("++ exprToString l++" + "++ exprToString r++")" exprToString (Node Times l r) = "("++ exprToString l ++" * "++ exprToString r++")" Alternativ als show-Instanz. a1s = "((5 + 3) * 2)" : string Übung: Implementieren Sie exprToString als Instanz von foldTree! Übung: Schreiben Sie eine bessere Ausgabefunktion, die Klammern nur verwendet wenn nötig! Martin Hofmann, Steffen Jost Programmierung und Modellierung 08-18 Linearisierung Breitendurchlauf Arithmetische Ausdrücke Ungültige Ausdrücke Die Binärbaum-Repräsentation erlaubt ungültige Ausdrücke (engl. malformed expressions). ∗ 5 + 2 3 bad = Node Times (Node ( Const 5) (leaf Plus) ( Const 3)) (leaf ( Const 2)) Zahlen sollen nur als Blattknoten auftreten! Operationen nur als innere Knoten! Martin Hofmann, Steffen Jost Programmierung und Modellierung 08-19 Linearisierung Breitendurchlauf Arithmetische Ausdrücke Arithmetische Ausdrücke als Datentyp Wir definieren einen speziellen Baumtyp expr: data Expr = Const Integer | Plus Expr Expr | Times Expr Expr deriving (Eq) a2 = Times (Plus ( Const 5) ( Const 3)) ( Const 2) Die Ausdrucksrepräsentation ist ökonomischer. Die Funktionsdefinitionen sind auch klarer! eval eval eval eval :: Expr -> Integer ( Const n) = n (Plus l r) = eval l + eval r ( Times l r) = eval l * eval r Martin Hofmann, Steffen Jost Programmierung und Modellierung 08-20 Linearisierung Breitendurchlauf Arithmetische Ausdrücke Parsing Jetzt wollen wir Ausdrücke einlesen! Wir verwenden einen Datentyp von “Token” data Token = CONST Integer | LPAREN | RPAREN | PLUS | TIMES Als String notierte Ausdrücke entsprechen Token-Listen, z.B. s1 = [ CONST 3, TIMES , LPAREN , CONST 8, PLUS , CONST 3, RPAREN , PLUS , CONST 5, TIMES , CONST 4] entspricht "3 * ( 8 + 3 ) + 5 * 4" Die Aufgabe, aus einer Zeichenkette solch eine Token-Liste zu erzeugen, heißt lexikalische Analyse, oder lexing. Übung: Implementieren Sie eine Funktion lex, die das tut. lex :: String -> [ Token ] Martin Hofmann, Steffen Jost Programmierung und Modellierung 08-21 Linearisierung Breitendurchlauf Arithmetische Ausdrücke Parsing durch rekursiven Abstieg Die Aufgabe, aus einer Token-Liste einen Syntaxbaum zu erzeugen, heißt Parsing, oder Syntaxanalyse. Man kann die arithmetischen Ausdrücke durch folgende BNF-Grammatik beschreiben: expr ::= prod | prod + expr prod ::= factor | factor * prod Punkt-vor-Strich und factor ::= const | ( expr ) Klammerung wird von dieser Grammatik richtig behandelt. expr prod factor * + expr prod factor 3 ( expr factor 8 prod factor 3 Martin Hofmann, Steffen Jost prod ) prod + expr factor * prod 5 factor 4 Programmierung und Modellierung 08-22 Linearisierung Breitendurchlauf Arithmetische Ausdrücke Parsing durch rekursiven Abstieg Die Syntaxanalyse orientiert sich an dieser Grammatik. Wir schreiben drei gegenseitig rekursive Funktionen parseExpr :: [ Token ] -> (Expr ,[ Token ]) parseProd :: [ Token ] -> (Expr ,[ Token ]) parseFactor :: [ Token ] -> (Expr ,[ Token ]) wobei parseExpr l versucht, ein möglichst großes Anfangsstück von l als expr zu interpretieren. Ebenso: parseProd, parseFactor. parseExpr l = let (summand1 , rest1 ) = parseProd l in case rest1 of PLUS: rest2 -> let (summand2 , rest3 ) = parseExpr rest2 in (Plus(summand1 , summand2 ), rest3 ) _ -> (summand1 , rest1 ) Martin Hofmann, Steffen Jost Programmierung und Modellierung 08-23 Linearisierung Breitendurchlauf Arithmetische Ausdrücke Zusammenfassung Bäume als induktiv definierte mathematische Objekte. Bäume als rekursiver Datentyp Rekursive Funktionen auf Bäumen Linearisierungen: Vor-, In-, Nachordnung Tiefen- und Breitensuche für Bäume Bäume als Repräsentation von Syntax. Lexikalische und Syntaxanalyse. Martin Hofmann, Steffen Jost Programmierung und Modellierung 08-24