Heute möchte ich Ihnen eine Verfeinerung der Baumstruktur vom letzten Mal vorstellen, die AVL-BĂ€ume. Benannt nach ihren Erfindern, den Herren Adelson-Velskii und Landis, beheben Sie ein groĂes Manko, welches botanische und informatische BĂ€ume immer besitzen: 'Sie wachsen wild!'
Sinn und Nutzen der AVL-BĂ€ume
Dazu betrachten wir einige mögliche BĂ€ume, die durch das EinfĂŒgen von sieben Elementen in die Baumstruktur vom letzten Mal entstehen können.
Wie Sie in Abbildung 5a leicht erkennen, sieht, in AbhĂ€ngigkeit von der Eingabereihenfolge, das Aussehen der resultierenden BĂ€ume sehr unterschiedlich aus. Bei der linken Eingabesequenz sehen Sie, daĂ der Baum zu einer linearen Liste wird. Dabei geht der groĂe Vorteil der SuchbĂ€ume, die kurzen Suchwege, verloren. Deshalb nennt man diesen Baum und andere BĂ€ume, denen man die Baumstruktur nicht mehr ansehen kann, degenerierte BĂ€ume.
Bild 5a: Von degenerierten, vollstĂ€ndig und nur ein "biĂchen" angeglichenen BĂ€umen
Die Aufgabenstellung ist es nun, eine Möglichkeit zu finden, nach Ein- und AusfĂŒgen in BĂ€umen, durch Ausgleichsmechanismen, wieder kurze Suchwege zu erreichen, also die Degenerierung von BĂ€umen zu verhindern. Dabei ist der rechts in Abbildung 5a dargestellte Idealzustand, in der Regel, nur mit groĂem Aufwand herzustellen. Deshalb benötigen wir zunĂ€chst eine etwas schwĂ€chere Definition der Ausgeglichenheit, die effektive Algorithmen erlaubt. Dazu betrachten wir eine Definition fĂŒr einen ausgeglichenen Baum, die auf die, schon erwĂ€hnten, Herren A.V.L. zurĂŒckgeht:
Ein Baum wird genau dann als 'ausgeglichen' bezeichnet, wenn fĂŒr alle seine Knoten gilt, daĂ sich die Höhen des linken und des rechten Teilbaumes um nicht mehr als eins unterscheiden.
Betrachtet man, mit Augenmerk auf diese Definition, die BĂ€ume der Abbildung 5a, ist klar: Der rechts dargestellten Idealzustand, auch als vollstĂ€ndig ausgeglichen bezeichnet, ist ein AVL-Baum. Aber auch schon der mittlere Baum gibt sich, nach kurzem Hinschauen, als AVL-Baum zu erkennen. Gerade dieser kleine Spielraum, zwischen ausgeglichen und vollstĂ€ndig ausgeglichen, macht die Anzahl der, nach Ein- und AusfĂŒgen in BĂ€umen, notwendigen AusgleichsvorgĂ€nge, beim AVL-Verfahren, vertretbar.
Balance-Faktoren
Bevor wir uns nun an die einzelnen Mechanismen machen, möchte ich kurz auf die Höhenermittlung der TeilbÀume eingehen. Wie Sie sicherlich leicht einsehen, ist es völlig unwirtschaftlich, nach jeder Operation, die die Baumstruktur verÀndert, die LÀnge sÀmtlicher TeilbÀume zu ermitteln. Statt dessen bietet es sich an in die Baumstruktur, speziell in jedem Baumelement, einen zusÀtzlichen Merker einzubauen, der die Höhendifferenz des rechten und des linken Teilbaums verzeichnet. Da wir es mit AVL-BÀumen zu tun haben, hat dieser Merker nur drei mögliche Werte:
-1 : Der linke Teilbaum ist um eins höher.
0 : Beide TeilbÀume sind gleichhoch.
+1 : Der rechte Teilbaum ist um eins höher.
Diesen Merker, bal genannt, werde ich zusĂ€tzlich in die b_element-Definition der letzten Folge aufnehmen. Zusammen mit den, an unsere heutige Anwendung - eine AdreĂdatei - angelehnten SchlĂŒĂel- und Datentypdefinitionen, ergeben sich die Typen des Listings 5a.
An den Operationen der letzten Folge bewirken diese Vereinbarungen folgende Ănderungen:
Bild 5b: Vier Ausgleichssituationen beim EinfĂŒgen und ihre Symmetrie
EinfĂŒgen
Das EinfĂŒgen in AVL-BĂ€ume unterscheidet sich zunĂ€chst in keiner Weise von dem EinfĂŒgen in einfache binĂ€re BĂ€ume. Auch hier muĂ, ĂŒber die rekursive Baumsuche, der Suchpfad bis zur EinfĂŒgestelle ermittelt werden. Nach dem EinfĂŒgen, ist aber zusĂ€tzlich die HöhenĂ€nderung zu berĂŒcksichtigen. Dazu wird in die Parameterliste, der Funktion put_in (rekursive Subprozedur von insert), zusĂ€tzlich ein bool'scher Parameter h aufgenommen, der wahr wird, wenn eine HöhenĂ€nderung stattgefunden hat, falsch, sonst.
Anmerkung: Nach dem EinfĂŒgen ist h zunĂ€chst immer wahr, weil auf jeden Fall eine BalancefaktorĂ€nderung des Vaterknotens der EinfĂŒgestelle auftritt.
Der rekursive Charakter von put_in kommt uns nun sehr zu statten. Wir mĂŒssen nur nach dem rekursiven Aufruf von put_in eine Abfrage auf eine HöhenĂ€nderung starten. Hat eine HöhenĂ€nderung stattgefunden, so bestehen prinzipiell drei Möglichkeiten:
- Die HöhenÀnderung eines Teilbaumes gleicht ein, zuvor beste hendes, Ungleichgewicht des AVL-Baumes aus. In diesem Fall wird der Balancefaktor '0' und in den höher liegenden Knoten findet keine HöhenverÀnderung mehr statt, h ist also auf 'false' zu setzen.
- Die HöhenĂ€nderung trifft bei einem Knoten mit gleich hohen TeilbĂ€umen ein. Hier findet eine BalancefaktorĂ€nderung im Rahmen der AVL-Eigenschaft statt. Der Balancefaktor wechselt von '0' zu '-1', oder '+1', je nachdem, in welchem Teilbaum eingefĂŒgt wurde. Diese Ănderung bewirkt nun aber auch eine Ănderung der Teilbaumhöhe des nĂ€chst höheren Knotens.
h ist also auf dem Wert 'true' zu belassen.
- Die HöhenĂ€nderung verletzt die AVL-Eigenschaft. In diesem Fall sind Ausgleichsoperationen auszufĂŒhren.
Nach grĂŒndlicher Ăberlegung kommt man - die Herren A.V.L. und nach einiger (!) Zeit auch ich - zu dem Ergebnis, daĂ sich, bis auf Symmetrie, nur zwei mögliche Ausgleichssituationen ergeben können. Diese beiden Situationen, und die symmetrischen Situationen, sind in der Abbildung 5b dargestellt. Entsprechend den Bewegungen, der an den Rotationen beteiligten Knoten, A-C, nennt man:
Fall 1a eine L-Rotation,
Fall 1b eine R-Rotation,
Fall 2a eine LR-Rotation,
Fall 2b eine RL-Rotation,
wobei L fĂŒr Links- und R fĂŒr Rechts-Rotationen steht.
Die gestrichelten Linien verdeutlichen sehr schön, wie die TeilbÀume, die die AVL-Eigenschaft verletzen, bei den entsprechenden Rotationen angehoben werden.
Im Listing findet man die Zeigerrotationen fĂŒr L in den Zeilen 77-80, fĂŒr LR in 84-97, fĂŒr R in 119-122 und fĂŒr RL in 126-139.
Bemerkenswert ist dabei, daĂ durch lediglich eine (!) Rotation die AVL-Eigenschaft wieder hergestellt wird. Bei der AVL-Löschoperation werden wir damit, im Regelfall, nicht auskommen. Doch zuvor noch ein kleines Beispiel zum EinfĂŒgen. Betrachten Sie die Abbildung 5c. Hier dargestellt ist der AVL-Baum, der durch EinfĂŒgen der Elemente 9, 1, 3, 5, 4 und 10 entsteht. Die Pfeile markieren dabei die Knotenelemente mit, nach dem EinfĂŒgen, verletzter AVL-Eigenschaft. Die Kreise deuten die Rotationen an und dĂŒrften somit die R-L-Namensgebung verdeutlichen.
Eine kleine technische Ănderung möchte ich auch nicht unerwĂ€hnt lassen. Um das Ăberlaufen, des Pascal-Stacks bei der new-Operation zu verhindern, wird in Zeile 155 berechnet, ob der benötigte Speicherplatz fĂŒr das neu hinzukommende Element ausreicht. Dabei kommen die beiden Pascal+ Operationen memavail und sizeof zum Einsatz. memavail berechnet den verfĂŒgbaren Speicher in Worten; sizeof das Format des ĂŒbergebenen Typs in Byte. Ist der Speicher (store) ausreichend, wird das Element eingefĂŒgt. Der bool'sche RĂŒckgabeparameter von insert bekommt den Erfolg, dieser Aktion, zugewiesen.
Bild 5c: EinfĂŒgen an einem Beispiel
Löschen
Auch die Löschoperation gestaltet sich nicht prinzipiell anders, als in der letzten Folge. Hier erfolgt ebenfalls zunÀchst die rekursive Baumsuche (in delete). ZusÀtzlich zur letzten Folge, ist hier, wie schon bei insert, eine bool'sche Hilfsvariable h mit in die Parameterliste aufgenommen worden. Diese zeigt jetzt allerdings eine Höhenminderung an.
Das AusfĂŒgen des Knotens selber gliedert sich wieder in drei FĂ€lle:
- Kein rechter Nachfolger
- Kein linker Nachfolger
- Zwei echte Nachfolger
FĂŒr den nicht-trivialen letzten Fall, ist diesmal, abweichend zur letzten Folge, eine rekursive AusfĂŒgeprozedur (del) angegeben. Die Zeigerrotation (balance1 und balance2), mit Aussehen Ă€hnlich den Rotationen beim EinfĂŒgen, sind diesmal, um redundanten Code zu vermeiden, als Prozedur formuliert. Ansonsten realisieren sie die gleichen Aufgaben, wie die Rotationen in insert. Der entscheidende Unterschied ist, daĂ beim Löschen eventuell mehr als eine Ausgleichsoperation durchgefĂŒhrt werden muĂ.
Eine weitere Ănderung gegenĂŒber dem letzten Mal, ist die Behandlung eines, oder mehrerer, Elemente, die ausgefĂŒgt werden sollen. Wurden sie bei letzten Mal lediglich aus dem Baum ausgeklinkt, so wird diesmal, ĂŒber die Variable del_list in der Parameterliste von remove, ein Zeiger auf die ausgefĂŒgte Elementliste ĂŒbergeben. Dadurch besteht nun die Möglichkeit, entweder, durch die aufrufende Programmstelle, die Speicherfreigabe der Elementnachfolgerliste zu betreiben, oder mit dieser Liste eine Sicherheitsabfrage zu durchlaufen, in der einzelne, oder alle, Listenbestandteile wieder in den Baum eingefĂŒgt werden können. Wie diese letzte Möglichkeit zu handhaben ist, wird sich noch in unserer heutigen Anwendung zeigen.
Andere AVL-Operationen
Die anderen AVL-Operationen unterscheiden sich nicht wesentlich von den Operationen der letzten Folge. Um die FehleranfĂ€lligkeit der Dateioperationen zu senken, sind diese jedoch mit einem IO-Laufzeitcheck versehen worden. Dazu sind im Kopf des Listing 5b die beiden externen Pascal+ Operationen io_check und io_result vereinbart worden. Mit io_check(false) wird, vor Beginn des Datenzugriffs auf Datei, der IO-Check abgeschaltet. Dies bewirkt, daĂ sich Pascal+ nicht bei jedem kleinen IO-Fehler verabschiedet und, im AnschluĂ an einen Dateizugriff, die Fehlerkennung, mit io_result, erfragt werden kann. Entsprechend der Möglichkeit eines fehlerhaften Dateizugriffs, wurden die beiden dazu vorgesehenen AVL-Operationen, load und save, als Funktionen mit bool'schem RĂŒckgabewert deklariert.
Von indizierten und sequentiellen Zugriffen
Ohne sie beim Namen genannt zu haben, wurden in der heutigen und in der letzten Folge bereits die zwei Arten von Zugriffen auf eine Datenbank behandelt. Es handelt sich dabei um:
A: Indizierte Zugriffe, die schnell ĂŒber irgendeine Organisationsstruktur (Index) erfolgen (BinĂ€rbaum, AVL-Baum, ...).
B: Sequentielle Zugriffe, die die kompletten Daten durcharbeiten und dabei mitunter recht langsam sind. Bei den Baumstrukturen sind dies die X-order-Zugriffe.
Mit einer besonderen, sequentiellen Form des Auffindens von Daten, möchte ich mich nun befassen.
Ausgangspunkt ist dabei die Idee einer AdreĂdatei, als Anwendungsbeispiel fĂŒr BĂ€ume. Da in so einer AdreĂdatei sĂ€mtliche Daten in Form von Strings gespeichert werden können, werden wir nun eine Möglichkeit behandeln, Daten zu finden, indem wir ein Suchmuster fĂŒr die entsprechenden Daten vorgeben und wĂ€hrend eines X-order-Durchlaufs dieses Suchmuster mit sĂ€mtlichen DatensĂ€tzen vergleichen. MĂ€chtigkeit erlangt dieses Konzept durch die Möglichkeit Wildcards ('*' und '?') bei den Suchmustern zu verwenden.
Beispiel: Vorausgesetzt die Typdefinitionen fĂŒr unsere AdreĂdatei, möchten wir uns sĂ€mtliche Dortmunder, die Peter heiĂen und in unserer AdreĂdatei gespeichert sind, ausgeben lassen. Als Suchmuster geben wir also vor:
Anrede : *
Vorname : *Peter*
Nachname : *
Strasse : *
Wohnort : *Dortmund*
Telefon : *
Eine Stringrelation
Was nun benötigt wird, um DatensĂ€tze mit derartigen Suchmustern vergleichen zu können, ist im Wesentlichen eine Stringrelation, die Wildcards berĂŒcksichtigt (Listing 5c, Zeilen 54-107).
Dabei gehen wir davon aus, daĂ einer der beiden Strings Wildcards enthalten darf, der Andere nicht. Beim Vergleich der beiden Strings wird nun versucht, beide, bis auf den leeren String ('') zu reduzieren, indem gleiche Buchstaben abgetrennt werden und mit den Reststrings ein rekursiver Aufruf der Stringrelation erfolgt. Wurde nun, in irgendeinem Teil des rekursiven Aufrufbaumes der Prozedur, eine Reduktion auf zwei leere Strings erzielt, so ist ein Flag zu setzen, das, zum einen den Abbruch der Rekursion veranlaĂt, und, zum anderen als Gleichheitsbit dient (equal).
Beispiel: norm_str = 'abcde' wild_str = 'ab?*'
Der rekursive Aufrufbaum hÀtte folgendes Aussehen:
durchlauf('ab?*','abcde')
durchlauf('b?*','bcde')
durchlauf('?*','cde')
durchlauf('*','de')
durchlauf('','e') und durchlauf('','')
Hierbei fĂŒhrt der zweite Teil, des durch '*' verursachten Doppelaufrufs, zu einer Auslöschung der beiden Strings, also zur Vergleichbarkeit der ursprĂŒnglichen Strings.
muster_inorder
Zum sequentiellen Durchlauf der Baumstruktur, habe ich den inorder-Durchlauf (Zeilen 109-132) gewĂ€hlt. ZusĂ€tzlich zu den beiden TeilbĂ€umen ist dabei noch die Nachfolgerliste zu durchlaufen (ebenfalls rekursiv). Auf den Knoten wird, in inorder-Position, die Stringrelation auf sĂ€mtlichen SchlĂŒĂel- und Datenstrings ausgefĂŒhrt. Dabei werden die entsprechenden Knoteninhalte mit den Werten einer globalen Variable muster verglichen. Bei der Variable muster handelt es sich um einen b_file_type, da die Organisationsvariablen fĂŒr die Baumstruktur weder bei den Dateioperationen, noch bei den Suchmustern benötigt werden.
Als Operation, die in die Rekursion eingeschachtelt ist, habe ich eine Elementausgabe gewĂ€hlt. Denkbar sind natĂŒrlich alle möglichen Operationen, denen lediglich gemeinsam ist, daĂ sie die Baumstruktur festhalten. Also beispielsweise 'Editieren des Datensatzes'. Dabei ist allerdings darauf zu achten, daĂ der SchlĂŒĂel nicht verĂ€ndert wird, denn sonst geht die Ordnung der Baumstruktur verloren.
eingabe und ausgabe
Zwei Operationen, die ebenfalls nicht fehlen dĂŒrfen sind die beiden Operationen eingabe und ausgabe zum Lesen und Schreiben eines Datensatzes. (eingabe wird auch zur Mustereingabe verwendet.)
main
Das Hauptprogramm beinhaltet nun, organisiert in einem zentralen CASE-Statement, die sieben Operationen, Eingabe, Löschen, Suche nach Muster, Suche nach Nachname, Speichern, Laden und Löschen.
Einen etwas mehr als trivialen Operationsaufruf, bieten aber nur die beiden Operationen Löschen und Suche nach Nachname.
Bei Löschen (CASE-Label 2) wird, nach dem AusfĂŒgen aus dem Baum, die dabei ausgefĂŒgte Elementenliste durchlaufen. Die DatensĂ€tze, die dabei als wertvoll erachtet werden, werden erneut in die Baumstruktur eingefĂŒgt, die Anderen werden freigegeben.
Bei der Suche nach Nachnamen (CASE-Label 4) ist, nach dem Auffinden eines Elementes mit search_first, die Elementnachfolgerliste, mit search_next, zu durchlaufen.
Vorausschau
In der nÀchsten Folge werde ich Sie mit einem etwas anderen Ansatz zur Suchpfadoptimierung in BÀumen bekannt machen, den sogenannten B-BÀumen.
Denjenigen unter Ihnen, denen die Botanik langsam zum Hals heraushÀngt, sei versichert, daà es sich dabei um den letzten Vertreter der Spezies Baum handeln wird.
Listings zu diesem Artikel