480 likes | 642 Views
Kapitel 6: Suchbäume und weitere Sortierverfahren 6.1 Binäre Bäume Die Klasse BinTree mit Traversierungsmethoden 6.2 Suchbäume 6.2.1 AVL Bäume 6.3 HeapSort und BucketSort 6.3.1 HeapSort 6.3.2 BucketSort. 6.1 Binäre Bäume (Fortsetzung). Eigenschaften:
E N D
Kapitel 6: Suchbäume und weitere Sortierverfahren 6.1 Binäre Bäume Die Klasse BinTree mit Traversierungsmethoden 6.2 Suchbäume 6.2.1 AVL Bäume 6.3 HeapSort und BucketSort 6.3.1 HeapSort 6.3.2 BucketSort
6.1 Binäre Bäume (Fortsetzung) Eigenschaften: • Maximale Höhe eines Binärbaumes mit n Knoten ist n-1. (Wenn jeder innere Knoten genau ein Kind hat, liegt eine lineare Kette vor.) • Minimale Zahl der Knoten eines Binärbaumes der Höhe h ist h+1. (Dito)
Maximale Zahl der Knoten eines Binärbaumes der Höhe h: N(h) := 2h+1 - 1 Knoten. Beweis: durch Induktion. • Minimale Höhe eines Binärbaumes mit n Knoten: O(log n) bzw. genauer: [log2(n+1)]+ - 1. Begründung: Sei h die minimale Höhe eines Binärbaumes mit n Knoten. Dann ist: 2 h- 1 = N(h -1) < n N(h)= 2 h+1- 1 also 2 h < n + 1 2 h +1 also h < log2(n+1) h+1
Satz: In einem nichtleeren Binärbaum T, dessen innere Knoten jeweils genau zwei Söhne haben, gilt #(Blätter(T)) = #(innere Knoten(T)) + 1. Beweis: durch Induktion über die Größe des Baumes T. Induktionsanfang: T Baum mit einem Knoten. Dann #(Blätter(T)) = 1, #(innere Knoten(T)) = 0. Also Beh. OK. Induktionsschluss: T Baum mit mehr als einem Knoten. Dann ist die Wurzel ein innerer Knoten. Seien T1 und T2 der linke und der rechte Teilbaum. Beide sind nicht leer. Nach Induktionsannahme #(Blätter(Ti)) = #(innere Knoten(Ti)) + 1 für i=1,2. Wir erhalten: #(Blätter(T)) = #(Blätter(T1)) + #(Blätter(T2)) = #(innere Knoten(T1)) + 1 + #(innere Knoten(T2)) + 1 = #(innere Knoten(T)) + 1.
ADT-Spezifikation (BinTree): algebra BinTree sorts BinTree, El, boolean ops emptyTree: BinTree isEmpty: BinTree boolean isLeaf: BinTree boolean makeTree: BinTree x El x BinTree BinTree rootEl: BinTree El leftTree, rightTree: BinTree BinTree sets BinTree = {<>} + {<L,x,R> | L,R BinTree, x El } functions emptyTree() := <> makeTree(L,x,R) := <L,x,R> rootEl(<_,x,_>) := x ... end BinTree.
"Linearisierung" von Binärbäumen(„Traversierungsmethoden“, Durchlaufen) : ops inOrder, preOrder, postOrder: BinTree List functions inOrder(<>) = <> preOrder(<>) = <> postOrder(<>) = <> (leere Liste) inOrder(<L,x,R>) = inOrder(L) + <x> + inOrder(R) preOrder(<L,x,R>) = <x> + preOrder(L) + preOrder(R) postOrder(<L,x,R>) = postOrder(L) + postOrder(R) + <x> wobei "+" die Listen-Konkatenation bezeichnet
Beispiel: Binärbaum zum Ausdruck ((12/4)*2) • inOrder: 12, /, 4, *, 2 • preOrder: *, /, 12, 4, 2 • postOrder: 12, 4, /, 2, *
Implementierung in Java public class BinTree { private Object val; private BinTree right; private BinTree left; // Konstruktoren: BinTree(Object x) { val = x; left = right = null; } BinTree(Object x) { val = x; left = right = null; } BinTree(Object x, BinTree LTree, BinTree RTree) { val = x; left = LTree; right = RTree; }
// Basismethoden (entspr. Signatur): public boolean isLeaf() { return ( this.left == null && this.right == null ) ; } public Object nodeVal() // entspr. "rootEl" { return this.val; } public void setNodeVal(Object x) // zusaetzlich { this.val = x; } public BinTree leftTree() { return this.left; } public BinTree rightTree() { return this.right; } public static boolean isEmpty(BinTree T) { return ( T == null ); } public static BinTree makeTree(BinTree L, Object x, BinTree R) { return new BinTree(x,L,R); } }
// Durchlaufen: public static LiLiS preOrder(BinTree T) { if ( isEmpty(T) ) return LiLiS.emptyList(); else return conc3(list1(T.nodeVal()), preOrder(T.leftTree()), preOrder(T.rightTree()) ); } Etc. // Hilfsmethoden fuer LiLiS-Objekte: private static LiLiS conc3(LiLiS L1, LiLiS L2, LiLiS L3) { return LiLiS.concat(LiLiS.concat(L1,L2),L3); } private static LiLiS list1(Object el) { PCell Cel = new PCell(el); return new LiLiS(Cel); }
Array-Darstellung: Neben einer Darstellung mit Zeigern auch direkt in einem Array: Für links-vollständige Binärbäume: Knoteninhalte in der folgenden Reihenfolge in einen Array: von oben nach unten und in jeder Ebene von links nach rechts. Knoten mit dem Index i: • Linker Nachfolger: Index 2 i. • Rechter Nachfolger: Index 2 i + 1. • Vorgänger: Index i div2.
Beispiel zur Array-Darstellung: Anmerkung: Das geht auch für nicht vollständige Binärbäume, aber dann gibt es Lücken im Array.
Verallgemeinerung auf nicht-binäre Bäume: Eine mögliche Definition: Definition: Ein BaumT ist ein Tupel T = ( x, T1 , ... ,Tk), wobei x ein zulässiger Knoteninhalt und Ti Bäume sind. Dabei ist auch k = 0 zugelassen. Der entsprechende triviale Baum besteht nur aus einem Knoten. (Aber: Bei diesem Ansatz gehört nullnicht zur Menge der Bäume!)
Bezeichnungen und Eigenschaften: • Grad eines Knotens: Anzahl seiner Kinder. • Grad eines Baumes T: grad(T) = max { grad(k) | k Knoten in T } • Die maximale Zahl von Elementen eines Baumes der Höhe h vom Grad d ist N(h) = (d h+1- 1) / (d - 1).
Implementierung: • Über Arrays mit Zeigern auf die Kinder (nur wenn der Grad beschränkt und nicht zu groß ist). • Über Zeiger/Binärbäume: Zeiger auf den am weitesten links stehenden Sohn und auf den Bruder rechts daneben, z.B. class TreeNode { private Object val; private TreeNode leftmostChild; private TreeNode rightSibling; ... }
6.2 Suchbäume Dictionary-Operationen: • member • insert • delete Ziel: diese effizient implementieren
Zum Vergleich: member bei geordneter Liste als Array: durch binäre Suche
Definition: Sei T ein binärer Baum. N(T) bezeichne die Menge der Knoten von T. Eine Abbildung m: N(T) D heißt Knotenmarkierung, wobei D ein Wertebereich mit einer vollständigen Ordnung ist. Ein binärer Baum T mit Knotenmarkierung m heißt genau dann Suchbaum, wenn für jeden Teilbaum T ' = ( L, x, R ) in T gilt: y aus L m(y) < m(x) y aus R m(y) > m(x) Beachte: alle Marken verschieden (Dictionary!).
Implementierung: Wir leiten die Klasse BinSearchTree von der Klasse BinTree ab. Erweiterung (gegenüber BinTree): Markierungsfunktion numVal: BinSearchTree int Dictionary-Operationen zu realisieren: ops member: El x BinSearchTree boolean insert: El x BinSearchTree BinSearchTree delete: El X BinSearchTree BinSearchTree
Insert Algorithmus insert(Objekt x, Baum T) { falls T = leer dann {makeTree(leer,x,leer); return}; falls ( m(x) < m(Wurzel von T) ) dann insert(x, linker Unterbaum von T) sonst insert(x, rechter Unterbaum von T) }
Member algorithmus member(Objekt x, Baum T) {falls T = leer return false; int k = m(x); int k´ = m(Wurzel von T); falls k = k´ return true; falls k < k´ return member(x, linker Unterbaum von T) sonst return member(x, rechter Unterbaum von T)}
Delete Löschen (delete) eines Elementes: (Evtl. Umstrukturierung des Baumes.) 1. Bestimme den Teilbaum T ', an dessen Wurzel sich das zu löschende Element befindet. 2. Falls T ' ein Blatt ist, ersetze T ' in T durch null. 3. Falls T ' nur einen Unterbaum ( T '' ) besitzt, ersetze T ' durch T ''. 4. Sonst entferne das kleinste Element (min) aus dem rechten Unterbaum von T ' (man beachte: min besitzt nur einen Unterbaum) und setze T '.val = min. (alternativ das größte Element aus dem linken Unterbaum)
Komplexitätsanalyse Sei n die Zahl der Knoten des Suchbaumes. Kosten eines Durchlaufens: O(n) Kosten von member, insert, delete: nichtkonstanter Anteil: Suchen der richtigen Position im Binärbaum entlang eines Pfades von der Wurzel O(Höhe des Baumes)
Best case: vollständiger Baum Höhe = O(log n). Komplexität der Operationen also auch nur: O(log n). • Worst case: linearer Baum (entsteht z.B. durch Einfügen von vorsortierten Elementen) Höhe = n-1. Komplexität der Operationen: für jede einzelne Operation: O(n), für den Aufbau des Baums durch Einfügen: O(n²).
Komplexitätsanalyse (2) • Average case: Komplexität der Operationen ist von der Ordnung der durchschnittlichen Pfadlänge (Mittel über alle Pfade in allen Suchbäumen mit n Knoten) = O(log n) (siehe Skriptum: direkte Abschätzung oder Berechnung über die Harmonische Reihe)
Gesucht: Mittlere Astlänge in einem durchschnittlichen Suchbaum. Dieser sei nur durch Einfügungen entstanden. Einfügereihenfolge: Alle Permutationen der Menge der Schlüssel a1, ... , an gleichwahrscheinlich. Diese wollen wir zunächst als sortiert annehmen. A(n) := 1 + mittlere Astlänge im Baum mit n Schlüsseln A(n) := mittlere Zahl von Knoten auf Pfad in Baum mit n Schlüsseln. Sei ai+1 das erste gewählte Element. Dann steht dieses Element in der Wurzel. Im linken Teilbaum finden sich i, im rechten n-i-1 Elemente. Linker Teilbaum ist zufälliger Baum mit den Schlüsseln a1 bis ai , rechter mit den Schlüsseln ai+2, ..., an. Die mittlere Zahl von Knoten auf einem Pfad in diesem Baum ist daher i/n · ( A(i) + 1) + (n-i-1)/n (A(n-i-1) + 1) + 1·(1/n).
Dabei sind die Beiträge der Teilbäume um eins vergrößert (für die Wurzel) und mit den entsprechenden Gewichten belegt. Der letzte Term betrifft den Anteil der Wurzel. Schließlich muss über alle möglichen Wahlen von i mit 0 ≤ i < n gemittelt werden. So erhalten wir A(n) = n-2 [ ∑0 ≤ i < n [i (A(i)+1) + (n-i-1)(A(n-i-1)+1) + 1] Aus Symmetriegründen ist der Anteil der beiden Terme A(i) und A(n-i-1) gleich, die konstanten Teile summieren sich zu n und 2(n-1)n/2 auf. Somit folgt A(n) = n-2( 2 ∑0≤i<ni A(i) + (n-1)·n + n) = 1+ 2n-2∑0≤i<ni A(i) Wir führen die Abkürzung S(n) = ∑0≤i<n i A(i) ein und erhalten A(n) = 1 + 2n-2 S(n-1) und S(n) - S(n-1) = n A(n) = n + 2 · S(n-1) / n , also die Rekursionsformel S(n) = n + S(n-1) · (n+2) / n.
Rekursionsformel: S(n) = n + S(n-1) · (n+2) / n Außerdem ist S(0) = 0 und S(1) = A(1) = 1. Nun wollen wir durch Induktion folgende Ungleichung beweisen: S(n) ≤ n (n+1) · ln (n+1) Sicherlich ist dies für n = 0 und n = 1 richtig. Einsetzen der Rekursionsformel für n-1 ergibt aber beim Schluss von n-1 nach n: S(n) = n + S(n-1) · (n+2) / n ≤ n+(n-1) ·(n+2) ln n = n(n+1) ln (n+1) + (n+1)n (ln n - ln (n+1)) - 2 ln n + n ≤ n(n+1) ln (n+1) - (n+1) n / (n+1) - 2 ln n + n < n (n+1) ln (n+1) Dabei haben wir ln n - ln (n+1) = -1/(n+θ), 0<θ<1, verwendet. Dann folgt aber A(n) = 1 + 2n-2S(n-1) ≤ 1+ 2 ln n = O( log n)
6.2.1 AVL-Bäume (nach Adelson-Velskii & Landis, 1962) Komplexität der Operationen member, insert, delete bei Suchbäumen im worst case: (n). Geht besser! Idee: Balancierte Bäume. Definition: Ein AVL-Baum ist ein binärer Suchbaum derart, dass für jeden Teilbaum T ' = < L, x, R > gilt: | h(L) - h(R) | 1 (Teilbaum balanciert, AVL-Eigenschaft). Oft wir auch an den Knoten h(.)+1 annotiert.
Ziele 1. Wie erhält man die AVL-Eigenschaft beim Einfügen und Löschen? 2. Wir werden für AVL-Bäume sehen: Komplexität der Operationen im worst case = O(Höhe des AVL-Baumes) = O(log n)
Erhaltung der AVL-Eigenschaft Nach Einfügen und Löschen muss dafür gesorgt werden, dass der neue Baum wieder die AVL-Eigenschaft hat: Rebalancieren. Mittels: Rotationen und Doppelrotationen
Rotation (hier für den Fall, dass der rechte Unterbaum nach einem insert rechts zu groß)
Doppelrotation (hier für den Fall, dass der rechte Unterbaum nach einem insert links zu groß)
Rebalancieren nach Einfügen: Entweder der Baum ist noch balanciert oder: Satz: Nach einer Einfügung genügt eine Rotation oder Doppelrotation des ersten* aus der Balance geratenen Teilbaums zur Wiederherstellung der Balance (AVL-Eigenschaft). (* : auf dem Weg vom eingefügten Knoten zur Wurzel). Denn: nach Rotation/Doppelrotation ist die ursprüngliche Höhe dieses Teilbaums wiederhergestellt!
Rebalancieren nach Löschen: Entweder der Baum ist noch balanciert oder: Satz: Nach einer Löschoperation kann der erste* aus der Balance geratenen Teilbaum durch eine Rotation oder Doppelrotation wieder in Balance (AVL-Eigenschaft) gebracht werden. (* : auf dem Weg vom entnommenen Knoten zur Wurzel). Aber: da die Höhe des Teilbaums sich dadurch um 1 vermindern kann, muss dies evtl. für den nächstgrößeren Teilbaum wiederholt werden, usw. bis zur Wurzel.
Zur Implementation • Bei der Suche nach einem aus der Balance geratenen Unterbaum muss man nur weitersuchen (d.h. zum nächsthöheren Knoten gehen), wenn der zuletzt besuchte Unterbaum seine Höhe geändert hat. • Um ohne zusätzliches Durchlaufen von Knoten herauszufinden, welche Teilbäume nicht balanciert sind, merkt man sich bei den Knoten zusätzlich entweder die Höhe des Teilbaums oder die Balance (z.B.in der Form:Höhe(linker Unterbaum) – Höhe(rechter Unterbaum) ). Diese Daten müssen natürlich beim Einfügen und Löschen auch immer entsprechend geändert werden. • Es sollte eine Vorgängerfunktion implementiert werden.
Komplexitätsanalyse – worst case Sei h die Höhe des AVL-Baumes. Suchen: wie beim Suchbaum, also O(h). Einfügen: insert wie beim Suchbaum (O(h)) und vom eingefügten Knoten zur Wurzel zurück und höchstens eine (Doppel-) Rotation: auch O(h). Löschen: delete wie beim Suchbaum (O(h)) und vom eingefügten Knoten zur Wurzel zurück und dabei evtl. eine (Doppel-)Rotation pro Knoten: O(h). Also alle Operationen: O(h).
Abschätzung der Höhe eines AVL-Baumes Konstruktionsprinzip Sei N(h) die minimale Zahl der Knoten eines AVL-Baumes der Höhe h. N(0)=1, N(1)=2, N(h) = 1 + N(h-1) + N(h-2) für h 2. N(3)=4, N(4)=7 Erinnerung: Fibonacci-Zahlen fibo(0)=0, fibo(1)=1, fibo(n) = fibo(n-1) + fibo(n-2) fib(3)=1, fib(4)=2, fib(5)=3, fib(6)=5, fib(7)=8 Durch Nachrechnen zeigt man: N(h) = fibo(h+3) - 1 0 1 2 3
Sei jetzt n die Knotenzahl eines beliebigen AVL-Baums der Höhe h. Dann gilt: n N(h) , also mit p = (1 + sqrt(5))/2 und q = (1- sqrt(5))/2 n fibo(h+3)-1 = ( ph+3 – qh+3 ) / sqrt(5) – 1 ( p h+3/sqrt(5)) – 3/2, also h+3+logp(1/sqrt(5)) logp(n+3/2), also gibt es eine Konstante c mit h logp(n) + c = logp(2) • log2(n) + c = 1.44… • log2(n) + c = O(log n).
Fazit Die Höhe eines AVL-Baumes ist also nach oben beschränkt durch: logp(2) • log2(n) + c = 1.44… • log2(n) + c. Anmerkung: Für die maximale Höhe H eines AVL-Baumes mit n Knoten gilt: N(H) n < N(H+1) Daraus folgert man ähnlich wie oben: H = logp(2) • log2(n) + c‘ = 1.44… • log2(n) + c‘. Die oben berechnete obere Schranke ist also scharf.
6.3.1 Heapsort Idee:Zwei Phasen: 1. Aufbau des Heaps 2. Ausgabe des Heaps Für aufsteigende Sortierung: Heap mit umgekehrter Ordnung, d.h. Maximum in der Wurzel (nicht Minimum). Heapsort ist ein in-situ-Verfahren.
Wiederholung: Heap mit umgekehrter Ordnung: • Für jeden Knoten x und jeden Nachfolger y von x gilt: m(x) m(y), • links-vollständig, d.h. die Ebenen werden von der Wurzel her gefüllt, und jede Ebene von links nach rechts, • Implementierung in einem Array, in dem die Knoten in dieser Reihenfolge abgelegt werden.
Zweite Phase: 2. Ausgabe des Heaps: n-mal das Maximum (in der Wurzel) herausnehmen (deletemax) und mit dem Element an der letzten Stelle des aktuellen Heaps vertauschen. Heap wird um ein Element kürzer und geordnete Teilfolge am Ende des Array wird um ein Element länger. Aufwand: O(n log n).
Erste Phase: 1. Aufbau des Heaps: Einfache Methode: n-mal insert Aufwand: O(n log n). Verbesserung: betrachte Array a[1 … n ] bereits als linksvollständigen Binärbaum und lass die Elemente in der Reihenfolge a[n div 2] … a[2] a[1] einsinken! (Die Elemente a[n] … a[n div 2 +1] sind schon in Blättern.)
Formal: Teilheap: Ein Teilarray a[ i..k ] ( 1 ik <=n ) heißt Teilheap, wenn gilt: für alle j aus {i,...,k} ist m(a[ j ]) m(a[ 2j ]) falls 2j k und m(a[ j ]) m(a[ 2j+1]) falls 2j+1 k Ist a[i+1..n] bereits ein Teilheap, so wird durch Einsinkenlassen des Elements a[i] auch a[i…n] zu einen Teilheap.
Aufwandsberechnung Sei k = [log n+1]+ - 1. Aufwand: Für ein Element in der Ebene j von oben: k – j. Insgesamt: {j=0,…,k} (k-j)•2j = 2k•{i=0,…,k} i/2i =2 • 2k = O(n).
Vorteil: Neue Aufbaustrategie effizienter! Anwendung: Wenn nur die m größten Elemente ausgegeben werden sollen: 1. Aufbau in O(n) Schritten. 2. Ausgabe der m größten Elemente in O(m•log n) Schritten. Gesamtaufwand: O( n + m•log n).
Nachtrag: Sortieren mit Suchbäumen Algorithmus: • Aufbau eines Suchbaums (z.B. AVL-Baums) aus der zu sortierenden Folge durch Einfügeoperationen. • Ausgabe in InOrder-Reihenfolge. sortierte Folge. Aufwand: 1. O(n log n) mit AVL-Bäumen, 2. O(n). Gesamt: O(n log n). Bis auf Konstanten optimal!