Datenstrukturen für Wörterbücher II: Natürliche Suchbäume

Werbung
Datenstrukturen für Wörterbücher II:
Natürliche Suchbäume
Tobias Pröger
24. November 2016
Erklärung: Diese Mitschrift ist als Ergänzung zur Vorlesung gedacht. Wir erheben keinen
Anspruch auf Vollständigkeit und Korrektheit. Wir sind froh über Hinweise zu Fehlern oder
Ungenauigkeiten. Bitte senden Sie diese an [email protected].
Wir haben mit Hashing bereits eine Implementierung von Wörterbüchern kennengelernt, die im besten sowie im durchschnittlichen Fall sehr schnelle Zugriffszeiten hat.
Auch haben wir überlegt, wie man erreichen kann, dass man den schlechtesten Fall
mit hoher Wahrscheinlichkeit umgehen kann. Schlimmer als eine lineare Suchzeit im
schlechtesten Fall ist jedoch, dass Hashing manche Anfragen überhaupt nicht unterstützt. Bisher haben wir nur das Suchen, Einfügen und Entfernen von Schlüsseln
diskutiert. In vielen realen Anwendungen möchte man aber beispielsweise auch die
gespeicherten Schlüssel in aufsteigend sortierter Reihenfolge ausgeben können. Ebenso möchte man vielleicht für einen Schlüssel k, der nicht im Wörterbuch enthalten
ist, den nächstkleineren Schlüssel k 0 finden, der im Wörterbuch enthalten ist. Beide
Operationen können mit Hashing nicht implementiert werden.
Wir haben bereits früher das Konzept eines binären Baums kennengelernt (z.B.
bei der unteren Schranke für allgemeine Sortierverfahren und bei Heaps). Ein binärer
Baum ist entweder
Motivation
Binärer Baum
• ein Blatt, d.h. der Baum ist leer, oder
• ein innerer Knoten v mit zwei Bäumen Tl (v) und Tr (v) als linkem bzw. rechtem
Nachfolger. Der Baum Tl (v) heisst linker Teilbaum von v, Tr (v) heisst rechter
Teilbaum von v.
Jeder Baum besitzt genau einen Knoten ohne Vorgänger, die sog. Wurzel, die in
den folgenden Algorithmenbeschreibungen Root genannt wird. In jedem inneren
Knoten v speichern wir
• einen Schlüssel v.Key,
• einen Zeiger v.Left auf den linken Nachfolgerknoten (also auf die Wurzel des
linken Teilbaums Tl (v) und nicht auf den Teilbaum als ganzes), sowie
• einen Zeiger v.Right auf den rechten Nachfolgerknoten (also auf die Wurzel
des rechten Teilbaums Tr (v)).
Ist der linke (bzw. rechte) Nachfolger eines inneren Knotens ein Blatt, dann setzen
wir v.Left (bzw. v.Right) auf null. Der gesamte Baum wird dann durch einen
Zeiger auf die Wurzel gegeben. Der Baum wird also vollständig durch Zeiger auf die
entsprechenden Nachfolger repräsentiert, und nicht z.B. als Array (wie bei Heaps).
1
Wurzel
Abb. 1 Ein möglicher binärer Suchbaum zur Schlüsselmenge {5, 7, 8, 10, 11, 15}. Innere Knoten werden durch Kreise, Blätter durch Rechtecke dargestellt. Die Wurzel ist der
Knoten 7 mit den Nachfolgerknoten 5 und 10.
Binärer
Suchbaum
Suchbaumeigenschaft
Trotzdem ist noch nicht klar, wie dies bei der Suche nach einem gegebenen
Schlüssel hilft. Ein Heap könnte z.B. auch durch entsprechende Zeiger repräsentiert
werden, aber es ist unklar, wie man dort effizient suchen kann. Daher führen wir
nun ein weiteres Kriterium ein: Ein binärer Suchbaum ist ein binärer Baum, der die
Suchbaumeigenschaft erfüllt: Jeder innere Knoten v speichert einen Schlüssel k, alle
im linken Teilbaum Tl (v) von v gespeicherten Schlüssel sind kleiner als k und alle
im rechten Teilbaum Tr (v) von v gespeicherten Schlüssel sind grösser als k.
Suchen eines Schlüssels Abbildung 1 zeigt einen möglichen binären Suchbaum
zur Schlüsselmenge {5, 7, 8, 10, 11, 15}. Man beachte, dass es im Allgemeinen sehr
viele verschiedene Suchbäume gibt, die die gleiche Schlüsselmenge repräsentieren.
So kann etwa jeder Schlüssel an der Wurzel stehen. Da aber alle Suchbäume die
Suchbaumeigenschaft erfüllen, können wir eine binäre Suche simulieren, um nach
einem gegebenen Schlüssel k zu suchen. Dazu starten wir an der Wurzel und prüfen,
ob k dort gespeichert ist. Falls ja, dann haben wir k gefunden und beenden das
Verfahren. Falls nein, dann fahren wir im linken Teilbaum von v fort, falls k kleiner
als der Schlüssel der Wurzel ist, und ansonsten im rechten Teilbaum. Stossen wir
auf ein Blatt, dann ist der Schlüssel k nicht vorhanden.
Schlüssel
suchen
Höhe
Search(k)
1 v ← Root
2 while v ist kein Blatt do
3
if k = v.Key then return true
4
else if k < v.Key then v ← v.Left
6
else v ← v.Right
7 return false
. Element gefunden
. Suche links weiter
. Suche rechts weiter
. k nicht gefunden
Zur Analyse der Laufzeit definieren wir die Höhe eines Baums T . Ist T ein Blatt,
dann ist die Höhe h(T ) = 0. Ist T ein Baum mit Wurzel v und den Teilbäumen Tl (v)
bzw. Tr (v), dann ist
h(T ) = 1 + max{h(Tl (v)), h(Tr (v))}.
(1)
Die Höhe gibt anschaulich an, aus wie vielen Ebenen der Baum besteht. Der in
Abbildung 1 dargestellte Baum etwa hat Höhe 4.
2
Abb. 2 Der Suchbaum aus Abbildung 1 nach der Einfügung des Schlüssels 9.
Abb. 3 Entfernen, Fall 1 (beide Nachfolger von v sind Blätter).
Es ist nun leicht zu sehen, dass die zuvor vorgestellte Suche im schlechtesten
Fall Zeit O(h) braucht, wenn h die Höhe des binären Suchbaums ist: Die Schritte 1
sowie 3–7 benötigen nur konstante Zeit. Jeder Weg von der Wurzel zu einem Blatt
hat maximal Länge h, also wird die Schleife im zweiten Schritt maximal h Mal
durchlaufen.
Einfügen eines Schlüssels Beim Einfügen eines Schlüssels k führen wir zunächst
eine Suche nach k durch. Wird k gefunden, dann wird der Schlüssel nicht erneut
eingefügt und eine Fehlermeldung ausgegeben. Ansonsten endet die Suche erfolglos
in einem Blatt. Dieses wird durch einen inneren Knoten mit dem Schlüssel k und
zwei Blättern ersetzt. Fügen wir beispielsweise den Schlüssel 9 in den Suchbaum
aus Abbildung 1 ein, dann besuchen wir die Schlüssel 7, 10 und 8, und fügen 9 als
rechten Nachfolgerknoten von 8 ein.
Da zum Einfügen im Wesentlichen nur eine Suche durchgeführt wird und die anschliessende Ersetzung eines Knotens nur Zeit O(1) kostet, kann in einen Suchbaum
der Höhe h in Zeit O(h) eingefügt werden.
Entfernen eines Schlüssels Es verbleibt zu zeigen, wie ein Schlüssel k aus einem
binären Suchbaum entfernt werden kann. Sei dazu v der Knoten, in dem k gespeichert
ist. O.B.d.A. sei v nicht die Wurzel des Baums und u der Vorgänger von v. Wir
unterscheiden drei Fälle.
1. Fall: Beide Nachfolger von v sind Blätter. Dann kann der Knoten v direkt gelöscht werden, d.h. der entsprechende Nachfolger von u wird durch ein Blatt
ersetzt (siehe Abbildung 3).
2. Fall: Genau ein Nachfolger von v ist ein Blatt. Sei w der innere Knoten, der der
Nachfolger von v ist. Der entsprechende Nachfolger von u wird durch w ersetzt
und v gelöscht (siehe Abbildung 4).
3
Laufzeit
Abb. 4 Entfernen, Fall 2 (genau ein Nachfolger von v ist ein Blatt). Der Fall, in dem w
der linke Nachfolger von v ist, wird analog aufgelöst.
Abb. 5 Entfernen, Fall 3 (kein Nachfolger von v ist ein Blatt).
Symmetrischer
Nachfolger
3. Fall: Kein Nachfolger von v ist ein Blatt. Dann enthalten sowohl der linke Teilbaum Tl (v) als auch der rechte Teilbaum Tr (v) mindestens einen inneren Knoten. Sei w der Knoten mit minimalem Schlüssel in Tr (v) (dieser heisst symmetrischer Nachfolger von v). Wird der in v gespeicherte Schlüssel durch den in
w gespeicherten Schlüssel ersetzt und anschliessend w gelöscht (siehe Abbildung 5), dann bleibt die Suchbaumeigenschaft erhalten (denn der Schlüssel in
w ist minimal unter allen in Tr (v) gespeicherten Schlüsseln). Der Knoten w hat
keinen linken Nachfolgerknoten, denn dort müsste ein Schlüssel von kleinerem
Wert gespeichert sein (und der Schlüssel von w wäre nicht minimal in Tr (v)).
Also tritt beim Löschen von w nur einer der beiden erstgenannten Fälle auf.
Zu einem gegebenen Knoten v kann der symmetrische Nachfolger gefunden werden, indem man genau einmal nach rechts läuft und danach so lange dem linken
Nachfolger folgt, bis dieser ein Blatt als linken Nachfolger besitzt. Dies führt zu
folgendem Algorithmus.
SymmetricSuccessor(v)
1
2
3
4
Symmetrischer
Vorgänger
w ← v.Right
. Gehe genau einmal nach rechts
x ← v.Left
. Folge danach dem linken Nachfolger
while x ist kein Blatt do w ← x; x ← w.Left
return w
. w ist symmetrischer Nachfolger von v
Natürlich könnte auch der symmetrische Vorgänger (der Knoten mit grösstem
Schlüssel in Tl (v)) gewählt werden. Die vorigen Überlegungen gelten dann analog.
4
Theorem 1 (Laufzeit des Entfernens). Sei T ein binärer Suchbaum der Höhe h.
Das Entfernen eines Schlüssels in T erfordert im schlechtesten Fall Zeit O(h).
Laufzeit des
Entfernens
Beweis. Der zu entfernende Knoten muss zunächst gefunden werden,
was in Zeit O(h) möglich ist. Danach tritt einer der Fälle 1 bis 3 auf. In
den ersten beiden Fällen werden lediglich Zeiger verändert und Speicher
freigegeben. Daher fällt für sie nur Zeit O(1) an. Im dritten Fall muss
der zunächst mithilfe des Algorithmus SymmetricSuccessor der symmetrische Nachfolger gefunden werden. Dies kostet maximal Zeit O(h),
da die Pfadlänge von der Wurzel bis zu einem Blatt höchstens h beträgt.
Danach werden lediglich die Schlüssel vertauscht und der symmetrische
Nachfolger gelöscht, was in Zeit O(1) möglich ist. Damit wird im dritten
Fall maximal Zeit O(h) benötigt.
Durchlaufordnungen für Bäume Sei T ein binärer Suchbaum mit der Wurzel v,
dem linken Teilbaum Tl (v) und dem rechten Teilbaum Tr (v). Wir können die Knoten
von T auf verschiedene Arten durchlaufen:
• In der Hauptreihenfolge (engl. Preorder) wird zunächst der in v gespeicherte
Schlüssel ausgegeben. Danach wird zuerst rekursiv mit Tl (v) fortgefahren und
anschliessend rekursiv Tr (v) verarbeitet.
Hauptreihenfolge
Preorder
• In der Nebenreihenfolge (engl. Postorder) wird zunächst Tl (v) rekursiv verarbeitet und danach Tr (v). Schliesslich wird der in v gespeicherte Schlüssel
ausgegeben.
Nebenreihenfolge
Postorder
• In der symmetrischen Reihenfolge (engl. Inorder) wird zunächst Tl (v) rekursiv
besucht, danach der in v gespeicherte Schlüssel ausgegeben und schliesslich
Tr (v) rekursiv besucht. Da alle Schlüssel in Tl (v) kleiner und alle Schlüssel in
Tr (v) grösser sind als der in v gespeicherte Schlüssel, gibt die symmetrische
Reihenfolge die in T gespeicherten Schlüssel in sortierter Reihenfolge aus.
Symmetrische
Reihenfolge
Inorder
Beispiel Die Knoten des binären Suchbaums aus Abbildung 2 werden in den folgenden Reihenfolgen durchlaufen:
• 7, 5, 10, 8, 9, 11, 15 bei Verwendung der Hauptreihenfolge,
• 5, 9, 8, 15, 11, 10, 7 bei Verwendung der Nebenreihenfolge,
• 5, 7, 8, 9, 10, 11, 15 bei Verwendung der symmetrischen Reihenfolge.
Zusammenfassung und Ausblick Sei T ein binärer Suchbaum mit Höhe h, der n
Schlüssel verwaltet. Binäre Suchbäume implementieren die Wörterbuchoperationen
in Zeit O(h). Ausserdem werden eine Reihe weiterer Operationen unterstützt, zum
Beispiel:
• Min(T ): Liefert den Schlüssel aus T mit minimalem Wert zurück. Diese Operation kann in Zeit O(h) realisiert werden, indem ausgehend von der Wurzel
von T so lange dem linken Nachfolgerknoten gefolgt wird, bis dieser ein Blatt
als linken Nachfolger besitzt.
Minimaler
Schlüssel
• Extract-Min(T ): Sucht und entfernt den Schlüssel mit minimalem Wert
aus T . Auch diese Operation kann in Zeit O(h) durchgeführt werden.
Minimum
extrahieren
5
Schlüssel
ausgeben
• List(T ): Liefert eine sortierte Liste der in T gespeicherten Schlüssel zurück.
Diese Funktion wird von einem Durchlauf in symmetrischer Reihenfolge in Zeit
O(n) realisiert.
Vereinigung
• Join(T1 , T2 ): Seien T1 und T2 zwei Suchbäume zu den disjunkten Schlüsselmengen K1 und K2 , und zusätzlich der maximale Wert eines Schlüssels in
K1 kleiner als der minimale Wert eines Schlüssels in K2 . Join(T1 , T2 ) berechnet einen binären Suchbaum zur Schlüsselmenge K1 ∪ K2 . Dazu führen wir
zunächst Extract-Min(T2 ) aus, erhalten einen Schlüssel k sowie den aus T2
resultierenden Baum T20 . Dann erzeugen wir einen neuen Baum, dessen Wurzel
den Schlüssel k speichert und die Teilbäume T1 sowie T20 besitzt. Die Laufzeit
beträgt O(h).
Verhalten im
schlechtesten
Fall
Die soeben diskutierten natürlichen binären Suchbäume sind problematisch, wenn
die Schlüssel (grösstenteils) in geordneter Reihenfolge eingefügt werden. Im schlechtesten Fall degenerieren sie zu einer linearen Liste, und die Laufzeiten für die Wörterbuchoperationen sind dann linear in der Anzahl der gespeicherten Schlüssel.
6
Herunterladen