Was bisher geschah abstrakter Datentyp : Signatur Σ und Axiome Φ z.B. ADT Menge zur Verwaltung (Finden, Einfügen, Entfernen) mehrerer Elemente desselben Typs Spezifikation einer Schnittstelle Konkreter Datentyp : Σ-Struktur, die Φ erfüllt Realisierung Ein abstrakter Datentyp repräsentiert die Menge aller seiner möglichen Realisierungen (konkreten Datentypen, die den ADT erfüllen). Datenstrukturen zur Verwaltung mehrerer Elemente desselben Typs: lineare Datenstrukturen: Arrays (schneller Zugriff auf jedes Element über Index) Listen (rekursiv) Stack, Queue (eingeschränkter Zugriff auf spezielle Elemente) hierarchische Datenstrukturen: Bäume (rekursiv) Spezialfälle: Binärbäume, Unärbäume = Listen 140 Wiederholung ADT Menge Ziel: Verwaltung (Finden, Einfügen, Entfernen) einer Menge von Elementen Sorten: Bool, Element, Menge (hier kurz für MengehElementi) Signatur: emptyset : isEmpty : Menge → contains : Menge × Element → add, remove : Menge × Element → Menge Bool Bool Menge Axiome (alle ∀-quantifiziert: ∀s ∈ Menge∀e ∈ Element): contains(add(s, e), e) = t, contains(remove(s, e), e) = f, isempty(add(s, e)) = f, Φ= add(add(s, e), e0 ) = add(add(s, e0 ), e), add(add(s, e), e) = add(s, e), . . . (+ Axiome der Booleschen Algebra) 141 Realisierungen des ADT Menge in linearen Datenstrukturen (Array): unsortiert : I I I contains O(n) (lineare Suche) add O(1) (anfügen) remove O(n) (Entfernen und Nachrücken) sortiert : I I I contains O(log(n)) (binäre Suche) add O(n) (sortiertes Einfügen und Platzschaffen) remove O(n) (Entfernen und Nachrücken) (Größe der Eingabe: Anzahl der Elemente) Aufwand zum sortierten Einfügen lohnt sich bei häufiger Suche (Ausführung von contains) 142 Wiederholung Binärbaum data BinTree a = Leaf | Branch {key::a, left::BinTree a, right::BinTree a} rekursive Funktionen auf Bäumen, z.B. Durchquerungen Beispiel: Rekursive Bestimmung aller Teilbäume Spezifikation: V: Eingabe Binärbaum t N: Ausgabe Liste aller Teilbäume von t rekursives Verfahren: subtrees :: BinTree a -> [BinTree a] subtrees t = case t of Leaf -> [t] (Branch k l r) -> t : (subtrees l) ++ (subtrees r) 143 Realisierung des ADT Menge durch Binärbäume Realisierung der Operationen des ADT Menge emptyset emptyset :: Eq a => BinTree a emptyset = Leaf Laufzeit: O(1) isEmpty isEmpty :: Eq a => BinTree a -> Bool isEmpty t = case t of Leaf -> True Branch k l r -> False Laufzeit: O(1) add (eine von vielen Möglichkeiten) add :: Eq a => add t x = case Leaf Branch k l r BinTree a -> a -> BinTree a t of -> Branch x Leaf Leaf -> Branch k (add l x) r Laufzeit: O(height(t)) mit height = Höhe des Baumes (Länge des längsten Wurzel-Blatt-Pfades) im ungünstigsten Fall (Baum ist Pfad): O(n) 144 Binärbäume: contains Spezifikation der Funktion contains (auf ADT Menge): V: Eingabe (M, x) mit Menge M ⊆ A und x ∈ A N: true, falls x ∈ M false, sonst Spezifikation der Funktion contains (auf Binärbaum): V: Eingabe (t, x) mit Binärbaum t mit Schlüsseln aus A (repräsentiert die Menge) und x ∈ A N: true, falls t einen Teilbaum Branch(k , l, r ) mit x = k enthält false, sonst in Haskell: contains :: Eq a => contains t x = case Leaf -> (Branch k l r) -> BinTree a -> a -> Bool t of False (k == x) || contains l x || contains r x Laufzeit: O(n) 145 Binäre Suchbäume Der Binärbaum t hat die Suchbaum-Eigenschaft gdw. für jeden Teilbaum t 0 = Branch(x, l, r ) von t gilt: I für jeden Schlüsselwert y eines inneren Knotens von l gilt y <x I für jeden Schlüsselwert v 0 eines inneren Knotens von r gilt x <y Ergebnis der Inorder-Durchquerung jedes binären Suchbaumes ist eine aufsteigend sortierte Folge Laufzeit für sortierte Ausgabe aller im Suchbaum mit n Knoten enthaltenen Schlüssel: O(n) 146 Binäre Suchbäume: contains Spezifikation der Funktion contains (auf binärem Suchbaum): V: Eingabe (t, x) mit binärem Suchbaum t mit Schlüsseln aus A (repräsentiert die Menge) und x ∈ A N: true, falls t einen Teilbaum Branch(k , l, r ) mit x = k enthält false, sonst Idee (rekursiv): IA: Der leere Baum Leaf enthält keinen Schlüssel. IS: Baum Branch(k , l, r ) enthält x falls I x = k oder I x < k und linkes Kind enthält x oder I x > k und rechtes Kind enthält x contains contains Leaf Branch if x :: Ord a => BinTree a -> a -> Bool t x = case t of -> False k l r -> if x == k then True else < k then contains l x else contains r x 147 Laufzeit der Suche Laufzeit von contains t x hängt von Größe der Menge (Anzahl n der Knoten in t) ab: I falls t einen Teilbaum Branch(x, l, r ) enthält: Länge des Pfades von Wurzel zu Teilbaum I falls t keinen Teilbaum Branch(x, l, r ) enthält: Länge des Wurzel-Blatt-Pfades (Höhe des Baumes t) höchstens Höhe des Baumes t (log(n) ≤ height(t) ≤ n) Suche im Suchbaum t in O(height(t)) (i.A. schneller als lineare Suche in einer Folge) 148 Extremwerte in binären Suchbäumen Spezifikation Minimum: V: Eingabe binärer Suchbaum t (Wurzel) N: Ausgabe m: minimaler in t vorkommender Schlüsselwert (undefiniert für leeren Baum) Idee: in jedem binären Suchbaum steht der minimale Schlüsselwert im äußeren linken Knoten (Minimum lässt sich ohne Schlüsselvergleich bestimmen.) mini :: Ord a mini t = case Leaf Branch k Branch k => BinTree a -> a t of -> undefined Leaf r -> k l r -> mini l Laufzeit höchstens Tiefe des Baumes t (log(n) ≤ tiefe(t) ≤ n) Maximum analog (äußerer rechter Knoten) 149 Einfügen in binäre Suchbäume Spezifikation add: V: Eingabe (t, x) mit binärem Suchbaum t mit der Schlüsselmenge {x1 , . . . , xn } und Schlüsselwert x N: Ausgabe binärer Suchbaum t 0 mit der Schlüsselmenge {x1 , . . . , xn , x} Idee (rekursiv): IA: Einfügen in leeren Baum durch Erzeugung eines Knotens IS: in jedem Teilbaum: I schon vorhandene Schlüssel nicht einfügen (Menge) I Schlüssel < Wurzelschlüssel in linkes Kind einfügen I Schlüssel > Wurzelschlüssel in rechtes Kind einfügen add :: Ord a => BinTree a -> a -> BinTree a add t x = case t of Leaf -> Branch x Leaf Leaf Branch k l r -> if x == k then t else if x < k then Branch k (add l x) r else Branch k l (add r x) 150 Iteriertes Einfügen Beim Einfügen der Elemente der Menge {2, 3, 5, 7} in verschiedenen Reihenfolgen in Leaf entstehen i.A. veschiedene Bäume Beispiel (Tafel): I 3,7,5,2 I 5,3,7,2 I 2,3,5,7 Daraus lässt sich ein Sortierverfahren ableiten. Sortieren durch Einfügen in binären Suchbaum: 1. schrittweises Einfügen aller Elemente einer Menge in einen zu Beginn leeren binären Suchbaum 2. Sortierte Ausgabe durch Inorder-Durchquerung des so entstandenen binären Suchbaumes 151 Laufzeit Einfügen Idee: Schlüsselwert x wird dort in t eingefügt, wo ihn der Suchalgorithmus (contains) in t finden würde: I falls x schon vorhanden: kein Einfügen I falls x noch nicht vorhanden (Blatt): Ersetzen des Blattes durch neuen Knoten Branch x Leaf Leaf also dieselbe Laufzeit wie contains: höchstens Höhe des Baumes t (log(n) ≤ height(t) ≤ n) Einfügen in einen binären Suchbaum mit n Knoten in O(height(t)) Laufzeit des abgeleiteten Sortierverfahrens: O(n height(t)), 152 Löschen aus binären Suchbäumen Spezifikation remove: V: Eingabe (t, x) mit binärem Suchbaum t mit Schlüsseln {x1 , . . . , xn } und Schlüsselwert x N: Ausgabe binärer Suchbaum t 0 mit Schlüsseln {x1 , . . . , xn } \ {x} mögliche Fälle: 1. x kommt nicht in t vor: Ergebnis t 0 = t 2. x ist Schlüsselwert eines Teilbaumes s in t mit zwei leeren Kindern: Löschen des Knotens s (Ersetzen durch Leaf) 3. x ist Schlüsselwert eines Knotens s in t mit einem leeren Kind: Ersetzen des Knotens s durch sein einziges nichtleeres Kind 4. x ist Schlüsselwert eines Knotens s in t mit zwei nichtleeren Kindern: Tausch der Schlüsselwerte in s und dem linken äußeren Knoten r des rechten Kindes von s (Minimum des rechten Kindes) Löschen des Knotens r (rekursiv) Warum hat der so entstandene Baum die Suchbaumeigenschaft? Laufzeit für Löschen aus binärem Suchbaum t: O(height(t)) 153 Binäre Suchbäume: remove remove :: Ord a => BinTree a -> a -> BinTree a remove t x = case t of Leaf -> Leaf Branch k Leaf Leaf -> if x == k then Leaf else t Branch k l Leaf -> if x == k then l else Branch k (remove l x) Leaf Branch k Leaf r -> if x == k then r else Branch k Leaf (remove r x) Branch k l r -> if x == k then Branch (mini r) l (remove r (mini r)) else if x < k then Branch k (remove l x) r else Branch k l (remove r x) 154 Laufzeiten in binären Suchbäumen I contains I add I remove in O(height(t)) Laufzeiten in Abhängigkeit von der Knotenzahl n = size(t): Extremfälle: I für Pfade (entartete Bäume) height(t) = size(t) Laufzeit der Operationen O(n) I für balancierte Bäume height(t) = log size(t) Laufzeit der Operationen O(log(n)) 155 Anwendung von Suchbäumen (Balancierte) binäre Suchbäume sind geeignet zum Speichern und schnellen Wiederfinden von Daten anhand ihnen zugeordneten Schlüsselwerten Binäre Suchbäume: I Realisierung des ADT Menge für linear geordnete Mengen I Realisierung von Wörterbüchern (= Mengen von Paaren (Schlüssel, Wert) mit eindeutigem Schlüssel aus einer linear geordneten Menge, z.B. ⊆ )), (= partielle Funktionen: Schlüsselbereich → Wertebereich): Operationen: contains, add, remove, jeweils von Paaren (Schlüssel, Daten) z.B. Telefonbucheinträge über Namen (alphabetisch geordnet), Studenten über Studentennummern N 156 Balancierte binäre Suchbäume Laufzeit für Suche, Einfügen, Löschen in Baum t mit n Knoten: O(height(t)) Ziel: Laufzeit für Suche, Einfügen, Löschen O(log(n)) Idee: balancierte Suchbäume, in denen die Tiefe jedes Blattes (etwa) gleich ist (also O(log(n))) 157 Vollständig balancierte Bäume Binärer Suchbaum t heißt vollständig balanciert, wenn in jedem Knoten Branch(x, l, r ) in t gilt: |size(l) − size(r )| ≤ 1 Tiefe jedes Knotens ≤ blog(n)c + 1 Beispiel (Tafel) 158 Operationen in vollständig balancierten Bäumen emptyset O(1) isEmpty O(1) contains O(log(n)) add, remove in je zwei Schritten: 1. Einfüge- und Löschoperation für balancierte binäre Suchbäume: O(log(n)) 2. Rebalancieren, d.h. Wiederherstellen der vollständigen Balance: i.A. aufwendig 159 AVL-Bäume (Adelson-Velskii, Landis, 1962) Binärer Suchbaum t heißt AVL-Baum, wenn in jedem Teilbaum Branch(k , l, r ) in t gilt: |height(r ) − height(l)| ≤ 1 (AVL-Eigenschaft) Optimierung: Speichern der Balance height(r ) − height(l) in jedem Knoten erlaubt schnellen Test Laufzeiten: contains O(log(n)) add, remove Summe der Laufzeiten für 1. add, remove in balancierten binären Suchbäumen: O(log(n)) 2. Wiederherstellung der AVL-Eigenschaft: ? 160 Rebalancieren in AVL-Bäumen notwendig bei Verletzung der AVL-Eigenschaft im Teilbaum Branch(k , l, r ) mit |height(l) − height(r )| = 2 geschieht nach Einfügen (als neues Blatt) oder Löschen (rekursives Ersetzen durch einziges Kind oder Minimum im rechten Teilbaum) evtl. Verletzung in mehreren Vorgängern des eingefügten Knotens Fälle vor dem Einfügen: Balance = 0: height(l) = height(r ) nach Einfügen keine Verletzung der AVL-Eigenschaft −1: height(l) = height(r ) + 1 I I nach Einfügen in r keine Verletzung der AVL-Eigenschaft nach Einfügen in l evtl. Verletzung der AVL-Eigenschaft +1: height(l) + 1 = height(r ) I I nach Einfügen in l keine Verletzung der AVL-Eigenschaft nach Einfügen in r evtl. Verletzung der AVL-Eigenschaft (Fälle −1 und +1 sind symmetrisch) 161 (Einfache) Rotation in AVL-Bäumen bei Verletzungen der AVL-Eigenschaft im tiefsten Teilbaum k der Form k = Branch(x, Branch(y , l, r ), s) mit height(l) − 1 = height(r ) = height(s) (Balance im Teilbaum mit Wurzelschlüssel x: −2, y: −1) Ersetzung von k durch lrotate(k ) = Branch(y , l, Branch(x, r , s)) (Linksrotation) analog (symmetrischer Fall): Verletzungen der AVL-Eigenschaft im tiefsten Teilbaum k der Form k = Branch(x, l, Branch(y , r , s)) mit height(l) = height(r ) = height(s) − 1 (Balance im Teilbaum mit Wurzelschlüssel x: +2, y: +1) Ersetzung von k durch rrotate(k ) = Branch(y , Branch(x, l, r ), s) (Rechtsrotation) 162 Doppel-Rotation in AVL-Bäumen bei Verletzungen der AVL-Eigenschaft im tiefsten Teilbaum der Form k = Branch(x, Branch(y , l, Branch(z, r , s)), t) mit Balance im Teilbaum mit Wurzelschlüssel x: −2, y: +1 Ersetzung von k durch lrrotate(k ) = Branch(z, Branch(y , l, r ), Branch(x, s, t)) Links-Rechts-Rotation besteht aus zwei aufeinanderfolgenden einfachen Rotationen: rrotate(Left(k )) danach lrotate(k ) symmetrischer Fall: k = Branch(x, t, Branch(y , Branch(z, l, r ), s)) mit Balance im Teilbaum mit Wurzelschlüssel x: +2, y: −1 Ersetzung von k durch rlrotate(k ) = Branch(z, Branch(x, t, l), Branch(y , r , s)) Rechts-Links-Rotation besteht aus zwei aufeinanderfolgenden einfachen Rotationen: lrotate(Right(k )) danach rrotate(k ) 163 Laufzeit der Operationen in AVL-Bäumen Rebalancieren (Rotationen): O(1) Höhe von AVL-Bäumen mit n Knoten: O(log(n)) contains O(log(n)) add O(log(n)) remove O(log(n)) 164