Schon in der fĂŒnften Folge von Algorithmen & Datenstrukturen, behandelte ich eine spezielle Baumstruktur, die gegen Degenerierung geschĂŒtzt ist: den AVL-Baum. In der heutigen Folge, werde ich Ihnen eine ebenso geschĂŒtzte Struktur vorstellen: den Bayer-Baum, oder kurz B-Baum.
Noch'n Baum, wieso?
Alle bisher betrachteten BĂ€ume entstammen der Gattung BinĂ€rbĂ€ume, da sie maximal zwei Nachfolger je Knoten besitzen. Was von der Struktur sehr schön, weil einfach, ist, hat einen groĂen Nachteil:
Wird wegen hohem Datenaufkommen eine Baumrepresentation auf Massenspeichern gewĂ€hlt, so sind BinĂ€rbĂ€ume in hohem Masse uneffektiv, da mit jedem Dateizugriff nur jeweils ein Knoten mit einem SchlĂŒsselelement in den Hauptspeicher geladen wird.
Abhilfe kann man sich verschaffen, indem man mehr als einen SchlĂŒssel in jeden Knoten aufnimmt.
Bezeichnung: Der Baum wird dann von einem BinÀrbaum zu eine Multiwegbaum. Die Knoten werden als Seiten bezeichnet. Als Blattseiten bezeichnen wir die Seiten, die keine Nachfolger besitzen. Die erste und oberste aller Seiten bezeichnen wir als Wurzelseite.
Dabei wird davon ausgegangen, daĂ die SchlĂŒssel auf jeder Seite, entsprechend der SchlĂŒsselrelation angeordnet sind. Ist nun, auf der besuchten Seite, der erwĂŒnschte SchlĂŒssel nicht auffindbar, so kann er, sofern es sich bei der Seite nicht um eine Blattseite handelt, zwischen seinem unmittelbaren VorgĂ€nger und seinem unmittelbaren Nachfolger gesucht werden. Zwei SpezialfĂ€lle gilt es dabei zu beachten: Zum einen können alle SchlĂŒssel der Seite gröĂer sein, als unser gesuchter SchlĂŒssel. In diesem Fall ist 'links' vom ersten (kleinsten) SchlĂŒssel weiterzusuchen. Analog können alle SchlĂŒssel kleiner sein als der Gesuchte. Hier ist 'rechts' vom letzten (gröĂten) SchlĂŒssel weiterzusuchen.
Einige grundsĂ€tzliche Ăberlegungen
Leicht erkennt man, daĂ ein Baum mit m SchlĂŒsseln maximal m+1- Nachfolger besitzen kann. Ebenso leicht ist einzusehen, daĂ eine gewisse Obergrenze fĂŒr die FĂŒllung der Seiten vorzugeben ist, da man sonst nicht wĂŒĂte, ob man einen zusĂ€tzlichen SchlĂŒssel in die Seite aufnehmen soll, oder ihn auf die Baumnachfolger verweist. Eine andere Ăberlegung fĂŒhrt zu den Definitionen des Herrn Bayer:
Unsere MultiwegbĂ€ume neigen nĂ€mlich dazu, genau so leicht zu degenerieren, wie BinĂ€rbĂ€ume ohne Ausgleichsmechanismen. Deshalb werden, Ă€hnlich den AVL-BĂ€umen, einige Limitierungen eingefĂŒhrt, deren Beachtung die Degenerierung von MultiwegbĂ€umen verhindert.
- Es wird die Ordnung n eines Baumes definiert.
- SĂ€mtliche Seiten, mit Ausnahme der Wurzelseite, mĂŒĂen zwischen n und 2n Elementen besitzen.
- Jede Seite ist Blattseite, oder besitzt genau m+1 Nachfolger, wobei m die Anzahl der SchlĂŒsselelemente der Seite ist.
- Alle Blattseiten liegen auf einer Höhe.
Datenbankcharakter
Um Ihnen heute eine möglichst effektive Struktur an die Hand zu geben, ist es notwendig, auf die Besonderheiten von Datenbanken, die ja wohl das Hauptanwendungsgebiet von Indexstrukturen sind, einzugehen.
ZunĂ€chst einmal: Zeigerstrukturen sind heute out. An ihre Stelle tritt der direkte Random-Access-Zugriff auf Diskettendatei. Was also in den letzten Folgen ein Pointer war, wird nun zu einer Recordnummer, also einer Zahl die den bezeichneten Datensatz in einer Diskettendatei charakterisiert. Eine weitere Besonderheiten ist die strikte Trennung zwischen Daten und SchlĂŒsseln, worunter deren Abspeicherung in unter-schiedlichen Dateien zu verstehen ist.
GrundsÀtzlich zu Random-Access unter Pascal+
Da Random-Access, wie oben gefordert, vom Vater Pascal's, Niklaus Wirth, nicht vorgesehen war, weichen wir heute ein wenig vom Standard ab. Entgegen der normalen Dateibehandlung in Pascal, nach der eine Datei nur streng sequenziell, will sagen Datensatz fĂŒr Datensatz abgearbeitet werden kann, bietet Random-Access, wie schon der Name sagt, wahlfreien Zugriff auf alle DatensĂ€tze der bezeichneten Datei.
Dies geschieht unter Pascal+ wie folgt: Eine Random-Access-Datei wird unter Pascal+ mit der reset- Standardprozedur zum Lesen und Schreiben geöffnet. Den beiden Standardoperationen get und put wird nun, als zusĂ€tzlicher Parameter, eine Recordnummer nachgestellt, die den ent-sprechenden Datensatz kennzeichnet. Zu beachten ist dabei, daĂ man bei den beiden Operationen nicht ĂŒber das Dateiende hinausschieĂen sollte, also auf nicht vorhandene Elemente zugreift. Lediglich erlaubt ist ein schreibender Zugriff mit put auf ein Element, was eine Position auĂerhalb der Datei liegt, also das AnhĂ€ngen eines neuen Elementes. Erschwert wird die Sache ein wenig dadurch, daĂ es seitens Pascals keine Prozedur gibt, mit der man die Recordanzahl bestimmen kann. (Zumindestens habe ich keine gefunden.)
Ohne Dokumentation, gebe ich Ihnen deshalb die Funktion dateilaenge (Listing 6b, Zeilen 28-72) an die Hand, mit der man, unter Angabe eines Dateinamens, der ĂŒbrigens auch Wildcards beinhalten darf, die erforderlichen Daten erhĂ€lt. Die DateilĂ€nge wird dabei, per call-by-reference, als zweiter Parameter zurĂŒckgegeben. Als bool'schen RĂŒckgabewert der Funktion erhĂ€lt man ein Flag, was das Vorhandensein der Datei beschreibt. Dividiert man nun die DateilĂ€nge durch das Format des Datentyps (sizeof(<Datentyp>), so erhĂ€lt man die Anzahl der DatensĂ€tze in der bezeichneten Datei. Wird noch der Recordoffset (Position des ersten Records. In Pascal+ die Position 0) in Random-Access-Dateien beachtet, so ist vom erhaltenen Wert noch eins abzuziehen, um den letzten Datensatz zu markieren.
Oder etwa nicht ?
Eine Kleinigkeit gilt es noch zu beachten: Es besteht nÀmlich ein Unterschied zwischen der virtuellen Datei (Menge der geschriebenen Daten) und der physikalischen Datei (Daten die auf dem Massenspeicher schon angekommen sind.).
Der Unterschied besteht genau im, von Pascal+ benutzten, Puffer fĂŒr Dateizugriffe. Hier können nĂ€mlich noch DatensĂ€tze 'rumliegen', die noch nicht geschrieben wurden und auf eine gröĂere Sendung warten, um effektiv weggeschrieben zu werden. Dieser an sich recht löbliche EffektivitĂ€tsgrundsatz, fĂŒhrt fĂŒr unsere Belange dazu, daĂ aus der physikalischen DateilĂ€nge nicht immer die Anzahl der DatensĂ€tze berechnet werden kann. Dieses kleine Problemchen lĂ€Ăt sich auf zwei Arten umgehen: Entweder wird die Diskettendatei, vor jeder dateilaenge-AusfĂŒhrung, mit close explizit geschloĂen, womit wir Pascal den Befehl geben seinen Dateipuffer zu leeren, oder die Anzahl der DatensĂ€tze wird nur bei der Initialisierung 'hart' ermittelt und in einer Variable abgespeichert. Im spĂ€teren Verlauf muĂ man dann nur die Variable entsprechend Ă€ndern. (Mit letzterer Variante werden wir uns auch rummĂŒhen.)
Beim AusfĂŒgen aus Random-Access-Dateien ist zu einem kleinen Trick zu greifen. Das AusfĂŒgen eines einzelnen Datensatzes aus einer Datei ist, wegen den dabei nötigen Datenverschiebungen, sehr uneffektiv. Deshalb nimmt man in sĂ€mtliche DatensĂ€tze ein zusĂ€tzliches Flag auf, um gelöschte DatensĂ€tze markieren zu können. Beim EinfĂŒgen ist dieses Flag auf true zu setzen und beim eigentlichen Löschvorgang auf false. Durch einen speziellen Packalgorithmus, kann man, nachdem mehrere EintrĂ€ge gelöscht wurden, die gĂŒltigen Daten (flag auf true) zusammenfassen und die ungĂŒltigen aus der Datei entfernen.
Doch dazu spÀter mehr.
Typstruktur
Welche Typen aus den unterschiedlichen obigen Ăberlegungen resultieren, sehen Sie im Listing 6a). Da wĂ€ren zunĂ€chst die beiden Typen key_type und data_type, die unseren normalen Typzerfall in SchlĂŒssel und Daten symbolisieren. Die beiden Typen page_ptr und data_ptr sind nur noch der Bezeichnung nach als Zeiger zu erkennen. Sie symbolisieren den Zeigercharakter, den man mit Verweisen auf Recordnummern erreicht, auch ohne Zeiger explizit anzugeben. Der nĂ€chste Typ, base_type, entspricht den EintrĂ€gen der Datenbank. Er beinhaltet sĂ€mtliche Typen (key & data), die es diesmal zu verwalten gilt. Weiterhin ist hier noch ein Zeiger next vorgesehen, der es uns, wie schon bei den BĂ€umen, erlaubt SchlĂŒsselgleichheit zu verarbeiten. ZusĂ€tzlich ist noch das oben erwĂ€hnte Archivierungsflag aufzunehmen.
Die Seiten der Indexdatei, page_type, sehen etwas komplizierter aus. Sie beinhalten zunĂ€chst auch ein Archivierungsflag. Die nĂ€chste Variable, anz, gibt den FĂŒllungsgrad der Seite an. ptr0 ist der Ă€uĂerste linke Zeiger der besprochenen Baumstruktur. Die SchlĂŒssel (key), ihre jeweiligen rechten Nachfolger (ptr), und die Zeiger auf die entsprechenden DatensĂ€tze in der Datenbank (record_nr), sind zusĂ€tzlich in einer Eintragsstruktur item_type zusammengefaĂt. Von diesen 'EintrĂ€gen' benötigen wir maximal Baumordnung mal zwei, also 1..nn StĂŒck. Es bietet sich eine ARRAY- Representation an, die Sie hier auch realisiert finden.
Beide Seitenarten sind nun noch global (!) als FILE OF ... zu deklarieren, wobei die unterschiedlichen Intentionen in die Namenswahl eingehen (Listing 6c, Zeile 20+21).
Ein kleines Beispiel fĂŒr eine B-Baumstruktur zweiten Grades, mit Verzeigerung in die Datenbanken, finden Sie in Abbildung 6a). Die ()-Zeiger stellen dabei die Verzeigerung der Indexseiten dar. Die ()-Zeiger symbolisieren die Zeiger von der Indexdatei zur Datenbank. Und, last but not least, zeigen die ()-Zeiger die VerknĂŒpfung innerhalb der Datenbank, zwecks Organisierung der Elementnachfolgerliste.

Wie weiter oben bereits angeklungen ist, benötige ich heute auch einige Konstanten.
Eine zusĂ€tzliche PrĂ€prozessor- Datei, fĂŒr die Konstanten, möchte ich Ihnen allerdings ersparen, zumal es mit Pascal+ ab Version 2.0 möglich ist Konstanten-, Typ-, und Operationsdefinitionen wild zu mischen.
FĂŒr die B-Baumkonstanten bemĂŒhen Sie also jetzt bitte unser heutiges Demoprogramm (Listing 6c, Zeilen 11-14). Die Konstante n beinhaltet den Baumgrad; nn den doppelten Baumgrad. Als Ersatz zur Konstante nil bei Pointern, fĂŒhre ich heute die Konstante leer fĂŒr ein nicht spezifiziertes Random-Access-Element ein. Weiterhin treffe ich die Konvention, daĂ das B-Baumwurzelelement immer in erster Dateiposition (0) anzutreffen ist, um mir eine zusĂ€tzliche Informationsdatei zu ersparen. Daher noch eine kleine Konstante, wurzel, zwecks besserer Lesbarkeit.
Die Operationen auf B-BĂ€umen im einzelnen (Listing 6b, ab Zeile 74):
create_database
Die Indexdatei und die Datenbank werden mit rewrite eröffnet, um sie neu zu erzeugen und dabei gegebenenfalls alte Inhalte zu löschen.
start_database
ZunĂ€chst werden die beiden Dateienderecords ermittelt (last_index und last_data). Auf das darauffolgende Ăffnen der beiden Dateien zum Random-Access-Zugriff (reset), erfolgt noch eine Fallunterscheidung, die zwischen einer leeren Datei und einer Datei mit mindestens einem Element unterscheidet. Entsprechend dem Ergebis dieser Fallunterscheidung, ist der Anker unseres B-Baumes (tree) entweder auf leer, oder auf wurzel zu setzen.
insert_data
Den Kern von insert_data bildet die Prozedur search. Mit ihr wird, Ă€hnlich der Suche in BinĂ€rbĂ€umen, eine rekursive Suche in den B-BĂ€umen durchgefĂŒhrt. Anders als bei den BinĂ€rbĂ€umen sind allerdings je Seite maximal nn Wegentscheidung zu treffen. Da nn frei wĂ€hlbar und so mitunter recht groĂ werden kann, bietet es sich an, eine binĂ€re Suche auf dem SchlĂŒsselarray item durchzufĂŒhren (Zeilen 189-197). Ist das Element gefunden worden (Zeile 198: l-r>1), dann ist am B-Baum keine Ănderung vorzunehmen. Das neue Element ist lediglich am Anfang der Nachfolgerliste in die Datenbank einzuhĂ€ngen (Zeilen 200-205). Andernfalls geht die rekursive Suche weiter (Zeilen 209-212), bis die rekursive Abbruchbedingung (Zeile 174) erfĂŒllt ist. Da uns die B-Baumdefinition Nummer 4 ein EinfĂŒgen unterhalb einer Blattseite verbietet, wird der neue Wert ĂŒber den Parameter v von search zurĂŒckgegeben. Der Wert true des Parameters h symbolisiert dabei, daĂ ein Wert ĂŒber den Parameter v zurĂŒckgereicht wurde und in einer höheren Seite einzufĂŒgen ist. Dies geschieht durch Check auf h nach dem rekursiven Aufruf von search (Zeile 213). In diesem Fall wird die Prozedur insert aufgerufen, die das EinfĂŒgen von v in die entsprechende Blattseite ausfĂŒhrt und gegebenenfalls, bei wiederholtem SeitenĂŒberlauf, einen neuen Wert fĂŒr v bestimmt. Im nicht-trivialen Fall des SeitenĂŒberlaufs (Zeilen 136-170) werden dabei einige Eintragsshiftereien vorgenommen, deren ge-naues Studium ich dem interessierten Leser ĂŒberlassen möchte, da die Details diesen ohnehin recht groĂen Beitrag wahrscheinlich sprengen wĂŒrden.
Im Rumpf der Prozedur insert_data wird, vor dem Aufruf von search, zunÀchst der Datenbankpuffer initialisiert und am Ende der Datenbank eingehÀngt. Dabei ist darauf zu achten, daà zuvor die Datensatzanzahl, last_data, inkrementiert wurde. Nach dem Aufruf von search besteht nun noch die Möglichkeit, daà sich noch ein Element in v befindet (Zeilen 231-249). Dieses Element wird dann das erste Element der neuen Baumwurzel. Hierbei haben wir auch noch darauf zu achten, daà unserer Vereinbarungen, die Wurzel betreffend (Wurzel ist immer in Position 0) eingehalten werden. Diese Begebenheit macht normalerweise die Vertauschung von zwei Indexseiten notwendig.
Einen allgemeinen Fall fĂŒr das EinfĂŒgen (und auch Löschen) eines Elementes in einem B-Baum zweiten Grades, finden Sie in Abbildung 6b). Unser Algorithmus versucht das neue Element (91) zunĂ€chst in die Ă€uĂerste linke Seite einzufĂŒgen. Da diese aber schon voll ist (Grad * 2 = 4), wird die Seite aufgeteilt. Das mittlere Element der alten Seite (87) wandert dabei ĂŒber den Parameter v unseres Algorithmus zurĂŒck und wird als Entscheidungselement fĂŒr die beiden neuen Seiten (61 66 / 91 98) in die obere Seite ĂŒbernommen. Hier wird dadurch wiederum ein Ăberlauf ausgelöst, der letztendlich zu einer Steigerung der Baumhöhe fĂŒhrt, indem eine neue Wurzelseite (36) angelegt wird.

Anmerkung: Die Random-Access-Struktur ist leider nicht ganz so pflegeleicht, wie unsere bisherigen Zeigerstrukturen. Deshalb muĂ man beim Arbeiten mit Random-Access sehr darauf bedacht sein, den Inhalt der Puffervariablen rechtzeitig (vor eventuellen Ănderungen) wegzuschreiben (put), um fehlerhafte Variablenbelegungen zu vermeiden. Die entsprechenden Programmstellen wirken dadurch allerdings nicht ganz so elegant, wie Sie das bisher gewohnt waren. Nicht bei insert, aber in einigen der nun folgenden Operationen wird sogar das Anlegen von Kopien der Puffervariablen notwendig, nĂ€mlich genau dann, wenn auf mehrere der Indexseiten zugegriffen werden muĂ.
delete_data
delete_data besitzt mit der Prozedur search einen Programmteil, der sich durch sehr Ă€hnlichen Aufbau die Namensgleichheit mit der Prozedur 'search' unter insert_data verdient. Es wird hier ebenfalls der rekursive Baumdurchlauf betrieben, nur das bei der Auffindung, bzw. Nichtauffindung eines Elementes andere Konsequenzen gezogen werden mĂŒĂen.
Wird ein Element nicht gefunden (Zeile 399, a=leer), dann ist der Job fĂŒr delete_data erledigt, was Sie an den durchweg negativen Bekundungen in den Zeilen 401-403 leicht feststellen (false/leer/false). Andernfalls ist unser Problem nicht so einfach.
GrundsĂ€tzlich haben wir beim AusfĂŒgen aus Bayer-BĂ€umen eine Ă€hnliche Situation, wie beim AusfĂŒgen aus den schon hinreichend bekannten BinĂ€rbĂ€umen. Der einfachste Fall, der auftreten kann, ist das AusfĂŒgen aus einer Blattseite (Zeilen 428-432). Hier muĂ eben nur die Seite entsprechend zusammengerĂŒckt werden. Befindet sich unser auszufĂŒgendes Element nicht auf einer Blattseite, so ist es, genau wie bei den BinĂ€rbĂ€umen durch den unmittelbaren VorgĂ€nger, oder Nachfolger in der Relation zu ersetzen. Der Abwechslung halber nehmen wir diesmal nicht den maximalen linken Nachfolger des rechten Blattelementes (lexikographische Nachfolger), sondern den maximalen rechten Nachfolger des linken Blattelementes (lexikographischen VorgĂ€nger). Diesen ermittelt uns die, in Ă€hnlicher Form schon hĂ€ufig benutzte, Prozedur del, die auf rekursiven Wegen die Nachfolgerliste hinabsteigt. Unten angelangt (q=leer; Bedingung in 399; AusfĂŒhrung in 386-393) ist der k-te Eintrag des Elementes a durch den letzten Eintrag der ermittelten Blattseite p zu ersetzen. Anders als bei den BinĂ€rbĂ€umen brauchen wir uns diesmal nicht um einen eventuellen Nachfolger des letzten Eintrages von p zu kĂŒmmern, den der existiert ja, laut B-Baumdefinitionen, nicht.
Die Krone der UnĂŒbersichtlichkeit wird nun von der Prozedur underflow erreicht, die benutzt wird, um Seiten mit weniger als n Blattelementen wieder aufzufĂŒllen, bzw. mit anderen Seiten zusammenzulegen. Sie wird immer dann angewandt, wenn man aus einem rekursiven Aufruf, entweder von search, oder von del zurĂŒckkommt und der Parameter h true ist und somit einen Unterlauf in der zurĂŒckgelassenen Seite aufweist. In diesem Fall ist der Prozedur underflow die Seite mit Unterlauf, a, deren VorgĂ€nderseite, c und die Eintragposition s mitzuteilen, die die genaue Stelle in c markiert, deren Nachfolger a ist. Die Seite b stellt nun den anderen direkten Vor- bzw. Nachfolger der Stelle s in c dar. Je nachdem, ob in a und b nun noch genĂŒgend Knoten vorhanden sind, um zwei Seiten zu fĂŒllen, findet entweder ein Ausgleich zwischen den beiden Seiten statt (Zeilen 298-308 und 333-345), oder die Seiten werden zu einer Seite zusammengefaĂt (Zeilen 312-319 und 349-356). Die genaue Verbalisierung dieser im GroĂen einsichtigen, im Detail aber unĂŒbersichtlichen Sache schenke ich mir und Ihnen.
ZurĂŒck zum Hauptblock von delete_data. Nach dem Aufruf von search und der hier erfolgten MiĂhandlungen unseres B-Baumes, besteht noch die Möglichkeit, daĂ ein Unterlauf in unserem Wurzelknoten aufgetreten ist. Wie Sie den bisherigen Beispielen sicherlich entnommen haben, ist dies bis zu einem gewissen Grad erlaubt:
Entgegen den Definitionen fĂŒr eine beliebige Seite, muĂ sich lediglich ein Element im Wurzelknoten befinden.
Wenn es aber auch an diesem einen Element mangelt (Zeile 459), muĂ etwas unternommen werden:
A: Bei leerem linken Nachfolger ist der Baum völlig entleert und das ist unserem Programm durch setzen der Variable tree auf leer mitzuteilen.
B: Bei einem linken Nachfolger, ist dieser zu unserer Wurzelseite zu machen und die ehemalige Wurzel ist als gelöscht zu kenn- zeichnen (Setzen des Archivierungsflag). Beachten Sie dabei bitte auch, daà die beiden DatensÀtze q und tree auch physikalisch ausgetauscht werden (Zeilen 469-472).
AbschlieĂend sind noch die Datenseiten des gelöschten SchlĂŒssels aus der Datenbank zu entfernen. Zu diesem Zwecke wird in einer WHILE Schleife die del_list durchlaufen, wobei die entsprechenden Archivierungsflags auf false gesetzt werden.
search_ptr
search_ptr stellt nun die dritte und letzte Variante des Suchens in B-BĂ€umen dar.
Sie tut weiter nichts, als die Random-Access-Adresse eines durch key bezeichneten Datenbankeintrages (nicht Index) zu ermitteln. Der prinzipielle Aufbau ist gleich denen bei insert_data und delete_data, nur das keine so verwegenen Operationen auf der ermittelten Seite ausgefĂŒhrt werden.
search_first und search_next
search_first sucht den ersten Datensatz mit bezeichnetem SchlĂŒssel key in der Datenbank. Hierzu wird die Funktion search_ptr benutzt. Der Erfolg oder MiĂerfolg wird ĂŒber einen bool'schen Funktionsreturn kundgetan. Weiterhin ist die globale Variable next_list zu erwĂ€hnen, die das Auffinden eines dem ersten Datensatz nachfolgendem Element mit search_next erst ermöglicht. Durch wiederholten Aufruf von search_next kann so die gesamte Elementnachfolgerliste durch-schritten werden.
print_tree
Die Prozedur print_tree stellt eine Variante des PrĂ€orderdurchlaufs durch eine B-Baumstruktur dar. ZunĂ€chst werden dabei sĂ€mtliche SchlĂŒsselelemente ausgegeben und hierauffolgend die Liste sĂ€mtlicher Nachfolger durchlaufen. Nebenbei wird dabei auch noch, je Element, die Elementnachfolgerliste in der Datenbank durchlaufen. Zusammen mit einer Baumtiefevariable, tiefe, die bei jedem rekursivem Abstieg inkrementiert wird und mit den SchlĂŒsselelementen ausgegeben wird, ergeben sich dabei recht anschauliche Baumdarstellungen.
Spielt man ein wenig mit den Baumordnungen herum, so kann man mit print_tree Informationen darĂŒber gewinnen, wie schnell B-BĂ€ume in AbhĂ€ngigkeit von ihrer Ordnung wachsen. In Abbildung 6c erkennen Sie, daĂ die Baumtiefe, schon bei leicht erhöhter Ordnung sehr stark abnimmt. SchluĂfolgerungen, die man dadurch ĂŒber die optimalen Ordnung gewinnen kann, sind aber sehr von der GröĂe der SchlĂŒssel und der Organisation auf DatentrĂ€gern abhĂ€ngig. Sie können aber in den meisten FĂ€llen davon ausgehen, daĂ eine Seite immer dann recht effektiv geladen wird, wenn das gesamte Seitenformat ungefĂ€hr der VerarbeitungsgröĂe auf dem benutzten Massenspeicher entspricht.
Abbildung 6c:
Baumtiefe im Zusammenhang mit dem Grad des Baumes (30 Baumelemente)
Grad 1: Grad 2:
1: 43 (28/91) 1: 43 (28/91)
2: 23 (66) 2: 9 (3) 23 (66) 28 (5/98/71)
3: 8 (84) 15 (99) 3: 7 (23) 8 (84)
4: 7 (23) 3: 15 (99) 20 (93/23)
4: 9 (3) 3: 24 (76) 27 (80/74)
4: 20 (93/23) 3: 29 (1) 32 (18)
3: 28 (5/98/71) 2: 50 (12) 60 (87) 64 (78)
4: 24 (76) 27 (80/74) 3: 45 (66) 46 (70) 48 (3)
4: 29 (1) 32 (18) 3: 61 (18) 63 (14)
2: 61 (18) 3: 66 (6) 73 (2) 80 (6)
3: 48 (3) 51 (98)
4: 45 (66) 46 (70) Grad 4:
4: 50 (12) 1: 24 (76) 45 (66) 63 (14)
4: 55 (90) 60 (87) 2: 7 (23) 8 (84) 9 (3) 15 (99) 20 (93/23) 23 (66)
3: 66 (6) 2: 27 (80/74) 28 (5/98/71) 29 (1) 32 (18) 43 (28/91)
4: 63 (14) 64 (78) 2: 46 (70) 48 (3) 50 (12) 51 (98) 55 (90) 60 (87) 61 (18)
4: 73 (2) 80 (6) 2: 64 (78) 66 (6) 73 (2) 80 (6)
Beispiel: Unser ST hat als GrundgröĂe fĂŒr Dateien das Cluster (1 kByte). Unser Variablenformat (SchlĂŒssel in integer) ergibt sich zu 8+8*nn Byte (kleiner Ăberschlag der Typensumme des RECORD page_type). Hieraus wĂŒrde sich eine 'gĂŒnstige' Ordnung von etwa 63 ergeben.
Testumgebung
Zur Testumgebung (Listing 6c) ist nicht viel zu sagen. Ăber einen trivialen Operationsaufruf hinaus, ist nur die Routine zur Erzeugung einer 'gewissen' Anzahl von ZufallseintrĂ€gen zu nennen, die einem beim Testen viel Tipperei abnimmt.
Packen
Den Packalgorithmus, zur Entfernung der leeren DatensÀtze, habe ich in einem gesonderten Programm untergebracht (Listing 6d).
Um zu sehen, wie nötig man das Packen mal wieder gehabt hat, besitzt dieses Programm zunĂ€chst zwei kleine Funktionen, die durch einfachen Durchlauf der jeweiligen Datei den prozentualen Anteil der gefĂŒllten DatensĂ€tze ermittelt (Zeilen 23-43 fĂŒr die Indexdatei, Zeilen 45-65 fĂŒr die Datenbank).
Das eigentliche Packen geschieht nun in mehreren Phasen.
A: abstand_besorgen
ZunĂ€chst werden die Anzahlen der aufeinanderfolgenden gefĂŒllten und leeren DatensĂ€tze der beiden Dateien in den Puffern p_index und p_data untergebracht. Dabei treffen wir die Konvention, daĂ immer mit einer Sequenz von gefĂŒllten DatensĂ€tzen begonnen wird. In den ungeraden Indizes der Puffer haben wir hiernach die LĂ€ngen der noch gĂŒltigen Sequenzen; in den geraden Indizes die LĂ€ngen der zu entfernenden Sequenzen.
B: abstand_kumulieren
Durch das RausschmeiĂen der leeren DatensĂ€tze, Ă€ndern sich die meisten (fast alle) der page_ptr und data_ptr in den verbleibenden DatensĂ€tzen. Um die neuen Indizes dieser DatensĂ€tze mit möglichst geringem Aufwand berechnen zu können, addieren wir die Anzahl der belegt/unbelegt Sequenzen, in den beiden Puffern, auf.
Und zwar auf vollgende Weise:
Von links beginnend, berechnen wir in den ungeraden Indizes die Gesamtanzahl aller (belegt und unbelegt) vorstehenden Sequenzen. In den geraden Indizes berechnen wir nur die Gesamtanzahl der vorstehenden unbelegten Sequenzen.
Mit new_index (bzw. new_data) können wir danach sehr leicht die neuen Indizes berechnen, indem wir die ungeraden Pufferstellen bis zur Ăberschreitung der alten Indexzahl entlangwandern. Der neue Wert ergibt sich dann sehr leicht durch Differenzbildung mit dem vorstehenden geraden ARRAY-Index.
C: index_packen und data_packen
In den eigentlichen Packroutinen, sind nun nur noch, unter Auswechslung sĂ€mtlicher alter Indizes durch die Neuen, die gĂŒltigen DatensĂ€tze in eine temporĂ€re Datei zu kopieren. Hiernach erfolgt nur noch die Umbenennung der TemporĂ€ren, in die alte Dateibezeichnung.
Vorausschau
So das wĂ€r's !!! Die BĂ€ume sind tod. In der nĂ€chsten Folge von Algorithmen & Datenstrukturen, werden Sie Hashing-Verfahren kennenlernen, die mitunter auch recht hilfreich bei der DV sind. Sie können aufatmen. Die Listings werden dabei nĂ€mlich merklich kĂŒrzer und durchsichtiger. Bis dann,
Listings zu diesem Artikel