A(n)

Werbung
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
1
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)
2
• 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
3
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.
4
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.
5
"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
6
Beispiel: Binärbaum zum Ausdruck ((12/4)*2)
• inOrder: 12, /, 4, *, 2
• preOrder: *, /, 12, 4, 2
• postOrder: 12, 4, /, 2, *
7
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; }
8
// Basismethoden (entspr. Signatur):
public boolean isLeaf()
{ return ( this.left == null && this.right == null ) ; }
public Object nodeVal()
{ return this.val; }
// entspr. "rootEl"
public void setNodeVal(Object x)
{ this.val = x; }
// zusaetzlich
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); } }
9
// 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); }
10
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 div 2.
11
Beispiel zur Array-Darstellung:
Anmerkung: Das geht auch für nicht vollständige
Binärbäume, aber dann gibt es Lücken im Array.
12
Verallgemeinerung auf nicht-binäre Bäume:
Eine mögliche Definition:
Definition: Ein Baum T 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 null nicht zur
Menge der Bäume!)
13
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).
14
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;
...
}
15
6.2
Suchbäume
Dictionary-Operationen:
• member
• insert
• delete
Ziel: diese effizient implementieren
16
Zum Vergleich:
Geordnete
Ungeordnete
Liste (Zeiger) Liste (Zeiger)
Geordnete
Liste als Array
insert
O(n)
O(1)
O(n)
delete
O(n)
O(n)
O(n)
member
O(n)
O(n)
O(log n)
member bei geordneter Liste als Array: durch binäre Suche
17
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!).
18
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
19
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) }
20
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)}
21
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)
22
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)
23
• 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²).
24
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)
25
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
26
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<n i A(i) + (n-1)·n + n) = 1+ 2n-2∑0≤i<n i 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.
27
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-2 S(n-1) ≤ 1+ 2 ln n = O( log n)
28
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.
29
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)
30
Erhaltung der AVL-Eigenschaft
Nach Einfügen und Löschen muss dafür gesorgt
werden, dass der neue Baum wieder die AVLEigenschaft hat:
Rebalancieren.
Mittels: Rotationen und Doppelrotationen
31
Rotation (hier für den Fall, dass der rechte
Unterbaum nach einem insert rechts zu groß)
32
Doppelrotation (hier für den Fall, dass der rechte
Unterbaum nach einem insert links zu groß)
33
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 (AVLEigenschaft).
(* : auf dem Weg vom eingefügten Knoten zur
Wurzel).
Denn: nach Rotation/Doppelrotation ist die
ursprüngliche Höhe dieses Teilbaums
wiederhergestellt!
34
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.
35
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.
36
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).
37
Abschätzung der Höhe eines AVL-Baumes
Sei N(h) die minimale Zahl der
Knoten eines AVL-Baumes der Höhe h.
Konstruktionsprinzip
0
N(0)=1, N(1)=2,
1
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
2 3
38
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).
39
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.
40
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.
41
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.
42
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).
43
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.)
44
Formal: Teilheap:
Ein Teilarray a[ i..k ] ( 1  i  k <=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.
45
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).
46
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).
47
Nachtrag: Sortieren mit Suchbäumen
Algorithmus:
1. Aufbau eines Suchbaums (z.B. AVL-Baums) aus der
zu sortierenden Folge durch Einfügeoperationen.
2. 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!
48
Herunterladen