Jeder kennt den GemĂŒsegarten des Informatikers. In der einen oder anderen Form sind uns die verschiedenen Sorten des âLieblingsgemĂŒsesâ Baum schon ĂŒber den Weg gelaufen. Meistens verursachten sie Kopfschmerzen der Art âwie war das noch gleich, wie ging das? nein nein, so war das nicht...â. Hier möchte ich Sie mit der neuesten, vielleicht besseren ZĂŒchtung bekanntmachen: den sogenannten âSkip Listsâ.
Keine Angst. Nicht allzu viel Theorie, jedoch einen einfachen Algorithmus werde ich vorstellen, der zudem noch universell einsetzbar ist. Ein paar SĂ€tze ĂŒber die Flora muĂ ich jedoch vorausschicken.
BĂ€ume
... sind im Informatikergarten auf den Kopf gestellt. Die Wurzel hĂ€ngt in der Luft, und die Ăste und BlĂ€tter baumeln normalerweise nach unten. Einen regelrechten Stamm gibt es nicht. (Viel treffender wĂ€re es, wenn man BĂ€ume besser als Wurzelwerk bezeichnen wĂŒrde, aber wir ĂŒbernehmen ja alles aus dem Englischen.) Man unterscheidet hĂ€ufig zwischen BĂ€umen und Graphen (letztere interessieren uns im folgenden nicht), wobei die BĂ€ume eigentlich eine Spezialisierung des GewĂ€chses Graph sind. In BĂ€umen steckt schon ein wenig mehr Semantik. Man assoziiert mit ihnen ein gewisses Aussehen und diverse Behandlungsmethoden. Doch dazu spĂ€ter mehr.
Die Stammsorte der BĂ€ume besitzt 2 Ăste (Bild 1b), von denen wir den einen âder Ordnung halberâ als seinen linken und den anderen als seinen rechten Sohn (schon wieder diese unzutreffenden Benennungen) bezeichnen.
Eine andere Sorte hat mehrere Ăste, respektive Söhne oder Nachfahren. Wollen wir also die beiden Typen unterscheiden, spricht man von binĂ€ren und n-Ă€ren BĂ€umen (Bild lb).
Bild 1a: BinÀrer Baum mit 2 Söhnen, Bild 1b: n-Àrer Baum mit n Söhnen
Jede Verzweigung (Knoten) im Baum enthĂ€lt einen Identifikationswert. HĂ€ufig wird er auch SchlĂŒssel (key) genannt. Bei manchen Baumsorten ist dieser SchlĂŒssel Teil der Information, die dann in jedem Knoten gespeichert wird. Teilweise besteht die Information nur aus diesem SchlĂŒssel. Andere Sorten enthalten nur in den BlĂ€ttern (Endknoten, Knoten, die keine Söhne mehr haben) Informationen. Aber alle Sorten besitzen in jedem Knoten SchlĂŒssel.
Ăber diese Informationen ist durch die Baumstruktur eine Ordnung definiert, und man legt im allgemeinen fest, daĂ alle Informationen in dem linken Sohn und all dessen Söhne kleiner als die Information des Knotens sind. Die der rechten Seite (alle weiter ârechtsâ liegenden Geschwister) sind alle gröĂer. Das Wort âkleinerâ hat es so in sich. Was bedeutet es, daĂ eine Information âkleinerâ als eine andere ist? Bei Zeichenketten bedeutet es wohl, daĂ eine Zeichenkette lexikalisch kleiner als eine andere ist. Bei Zahlen bedeutet es offensichtlich mathematisch kleiner, also âlinksâ auf dem Zahlenstrahl.
Kennt man die Definition von âkleinerâ, kann man in einem Baum sehr schnell nach Informationen suchen, indem man den SchlĂŒssel der zu suchenden Information mit demjenigen des gerade aktuellen Knotens vergleicht und es abhĂ€ngig von dem Ergebnis mit einem der Söhne weiterversucht.
Wo wachsen die BĂ€ume?
HĂ€ufig werden sie in arrays gepflanzt (Bild 2a). So z.B. die GEM-Resourcen von Programmen, die mit dem Resource-Construction-Set erstellt wurden. (Anmerkung: Ein Objekt in der Resource ist kleiner als das andere, wenn es sich grafisch gesehen innerhalb des anderen befindet.) Solche BĂ€ume wachsen in einem festen Beet und können somit eine bestimmte GröĂe, respektive die Anzahl der Verzweigungen nicht ĂŒberschreiten. Viel schöner, aber auch aufwendiger zu pflegen, sind die dynamischen BĂ€ume (Bild 2b), deren GröĂe von vornherein nicht feststeht, und die zur Laufzeit aufgebaut werden. Und mit diesen werden wir uns hier nĂ€her beschĂ€ftigen.
FĂŒr mathematisch Interessierte: Der Aufwand zum Suchen einer Information in einem n-Ă€ren Baum (ein Baum mit maximal n Söhnen) - und damit ist auch die Unterscheidung, ob es diese Information gibt oder nicht, gemeint - ist 0(log(m)) (Logarithmus zur Basis n. Es befinden sich maximal m Informationen in dem Baum). Zum Vergleich: Lineares Suchen, d.h. alle Informationen der Reihe nach untersuchen, dauert im ungĂŒnstigsten Fall, also, wenn alle Informationen zu untersuchen sind (z.B. die Information ist nicht vorhanden), m-Zeiteinheiten. Der Aufwand ist damit O(n). Bei einer groĂen Anzahl von Informationen macht sich das erheblich bemerkbar.
Bild 2a: ausgeglichener binÀrer Baum, Bild 2b: degenerierter Baum
Kranke und miĂratene BĂ€ume
Aber BĂ€ume sind beim Wachsen und wĂ€hrend ihrer Lebenszeit (in der sie manchmal auch gestutzt werden mĂŒssen) stĂ€ndigen Gefahren ausgesetzt. Beide Phasen eines Baumlebens mĂŒnden in dasselbe Krankheitsbild: degenerierte BĂ€ume. Beim Wachsen eines Baumes bilden sich immer neue BlĂ€tter, und es besteht die Möglichkeit, daĂ immer nur der rechte Ast Söhne bekommt und die anderen Söhne unfruchtbar bleiben. Resultat: der Baum âhĂ€ngt schiefâ (Bild 3b).
Dies wirkt sich fatal fĂŒr Suchalgorithmen aus. Einige Informationen werden erheblich schneller gefunden als andere. Die durchschnittliche Suchdauer ist gröĂer als bei einem optimal ausbalancierten Baum. Hoppla: optimal ausbalanciert ist ein Begriff, der erst mal erklĂ€rt werden muĂ.
Bildlich gesprochen bedeutet es. daĂ das Astwerk schön buschig ist, daĂ, um technischer zu werden, die maximale AstlĂ€nge sich von der minimalen AstlĂ€nge nur um ein MindestmaĂ unterscheidet. Im Normalfall bedeutet dies eine maximale Differenz von einer AstlĂ€nge. Die oben erwĂ€hnte mathematische Formel ist nur fĂŒr ausgeglichene BĂ€ume gĂŒltig (Bild 3a).
Insofern ist man bestrebt, diesen Fall nicht eintreten zu lassen. Manchmal lĂ€Ăt er sich aber nicht vermeiden. Auch wenn BlĂ€tter absterben, kann es passieren, daĂ davon immer nur ein Ast betroffen ist.
Bild 3a: Baum als array realisiert, Bild 3b: dynamischer Baum realisiert
Medizin fĂŒr BĂ€ume
Die Informatik hat [Adelson, Velskii und Landis sei Dank] Abhilfe durch sogenannte AVL-BĂ€ume geschaffen. Die benötigten Algorithmen garantieren, daĂ in der Wachstums- und Lebensphase keine Krankheitssymptome auftreten, d.h. die Differenz der LĂ€nge der Ăste zwischen zwei verschiedenen Söhnen eines jeden Vaterknotens maximal eine Einheit ist. Trifft diese Bedingung auch fĂŒr den Urvater, die Wurzel des Baumes, zu, spricht man von einem ausgeglichenen Baum.
Die Medizin ist jedoch nicht ohne Nebenwirkungen. Die entsprechenden Algorithmen benötigen Speicherplatz fĂŒr die Routinen und zur Laufzeit evtl, einen ausreichend groĂen Stack (wie groĂ ist das?) wegen des rekursiven Layouts der Funktion (Ok, heutzutage vemachlĂ€ssigbar und fĂŒr Besserwisser: Ich weiĂ auch, es gibt nichtrekursive Verfahren, aber siehe den nĂ€chsten Absatz). Weiterhin wird die Laufzeit (dito) oder um im Jargon zu bleiben, die DurchfluĂgeschwindigkeit der Informationen durch den Baum durch die zusĂ€tzlichen Testfunktionen gehemmt.
Und, die Arznei muĂ sorgfĂ€ltig hergestellt (programmiert) werden. Letzteres ist hĂ€ufig der Grund, auf diese Medizin zu verzichten. (Hoffen und Bangen, daĂ die einzufĂŒgenden Informationen in zufĂ€lliger Reihenfolge auftreten, da nur dies ein zufriedenstellendes Wachstum ohne Krankheitssymptome ermöglicht.)
Genmanipulation
Da dem nun so ist, versuchte William Pugh [1] einen anderen Ansatz. Und ĂŒber diesen möchte ich hier berichten. Das Problem lĂ€Ăt sich folgendermaĂen charakterisieren:
- Wie lĂ€Ăt sich ein Baum möglichst gleichmĂ€Ăig aufbauen, ohne daĂ man die SchlĂŒsselreihenfolge kennt oder gar beeinflussen kann?
- Oder wie lassen sich die Informationen in eine zufÀllige Reihenfolge bringen, die beim Einsortieren der Informationen einen ausgeglichenen Baum realisieren?
Eine Antwort: Der Aufbau des Baumes muĂ unabhĂ€ngig von den Informationen gestaltet werden, also unabhĂ€ngig von der Ordnung, respektive den SchlĂŒsseln der Informationen. Wie soll das gehen?
In einem Baum bisherigen Typs wird beim EinfĂŒgen mit Hilfe des SchlĂŒssels die Stelle gesucht, an der die Information eingefĂŒgt werden soll. Und genau diese Stelle mĂŒssen wir uns auf andere Weise unabhĂ€ngig von dem SchlĂŒssel besorgen.
Dabei ist natĂŒrlich die Sortierung des Baumes, bestimmt durch unsere âkleinerâ-Relation, zu beachten.
WĂ€hlen wir einen (nicht ganz passenden) Vergleich aus dem richtigen Leben: Wir suchen eine Telefonnummer in der dicken gelben Schwarte. Man greife also mit der rechten Hand unter das Buch, die Finger der linken Hand liegen auf dem Deckel, und mit dem Daumen hebt man eine âzufĂ€lligeâ Anzahl Seiten hoch. Wir stellen fest, daĂ unser Suchbegriff sich entweder im fĂŒhrenden Teil oder im nachfolgenden Teil des Buches befindet. Die Hand greift zwischen die aufgeschlagenen Seiten, und der Vorgang wiederholt sich mit dem Rest des Buches. (NatĂŒrlich wird irgendwann sequentiell geblĂ€ttert.) Probieren Sieâs mal bei diesem Heft mit dem Suchkriterium âSeitenzahlâ, ist ja dick genug.
Zentrale Eigenschaft dieses Algorithmusâ ist die ZufĂ€lligkeit, mit der wir auf eine bestimmte Seitenzahl stoĂen. Wir ĂŒberspringen dabei eine zufĂ€llige Zahl von BlĂ€ttern.
Pugh wÀhlte eine andere Beschreibungsmethode:
Durch eine aufsteigend sortierte Liste wird sequentiell nach einem Begriff gesucht. Stellen Sie sich eine StraĂe mit HĂ€usern vor. Die geraden Nummern auf der rechten, die anderen auf der linken Seite. Sie suchen, die StraĂe entlangradelnd (bei der Hausnummer 1 beginnend) Hausnummer ânâ. Dabei werden Sie natĂŒrlich nicht den Blick immer pendelnd zwischen den StraĂenseiten hin- und herbewegen, sondern immer nur eine Seite (in Deutschland bevorzugt die rechte) mit dem Blick schrĂ€g nach vorne betrachten, und im rechten Augenblick, nĂ€mlich dann, wenn man kurz vor der entsprechenden Nummer ist, den Kopf der richtigen Seite zuwenden. Dabei werden Hausnummern, die unleserlich oder nicht vorhanden sind, einfach auĂer acht gelassen. Wir ĂŒberspringen bei diesem Verfahren mindestens jede 2. Hausnummer.
Bild 4a: âskip listâ mit SprĂŒngen 2l
Bild 4b: âskip listâ mit willkĂŒrlichen SprĂŒngen
Wie wĂ€re es, wenn wir nicht jede 2. Hausnummer, sondern mehrere auf einmal ĂŒberspringen (dabei könnten wir schneller fahren), und erst kurz vor dem Ziel langsamer werdend, weniger Hausnummern ĂŒberspringend, genauer hinschauen wĂŒrden.
Begriffen? In dem Fall schauen Sie sich jetzt Bild 4a an. Dort ist eine sequentielle Liste mit dargestellt, mit jeweils verschiedener Anzahl von Verweisen auf weiter hinten liegende Elemente. Wenn wir in dieser Liste einen SchlĂŒssel (hier eine Zahl) suchen, folgen wir, von vorne beginnend, zuerst den Pfeilen, die am weitesten nach hinten zeigen (den oberen). Ist der SchlĂŒsselbegriff in der Information, auf die der Pfeil steht, kleiner oder gleich demjenigen, den wir suchen, setzen wir die Suche mit der Gruppe von Pfeilen fort, die PI mal Daumen nur noch halb so weit zeigen. Das setzen wir solange fort, bis wir entweder das Element gefunden haben, oder wir bei Level 0, d.h. der Pfeil zeigt auf das nĂ€chste Element in der sortierten Liste, angekommen sind. Alles klar? Das ist das Prinzip.
Was passiert, wenn wir einen neuen SchlĂŒsselbegriff einfĂŒgen möchten? Wir mĂŒssen uns nicht ĂŒberlegen, wo wir ihn einsortieren wollen (das legt ja unsere Ordnung âkleinerâ auf dem SchlĂŒsselbegriff fest), sondern ob wir ihn als Sohn oder als Enkel usw. behandeln wollen. Am besten, wir ĂŒberlassen dies dem Zufall. Und genau das ist die entscheidende Idee:
Instrumente
Wenn wir den Baum aufbauen, ĂŒberlassen wir es dem Zufall, auf welcher Ebene des Baumes ein neuer Knoten angeordnet wird - also ob er als Sohn, Enkel, Urenkel usw. behandelt wird.
Da der Zufall halt âzufĂ€lligâ ist, verteilen sich die SchlĂŒsselbegriffe auch zufĂ€llig auf die verschiedenen Ebenen der Nachfahren (Söhne, Enkel ...) unseres Baumes (wie Bild 4b zeigt). Setzen wir einen Zufallszahlengenerator voraus, der gleichverteilte Zahlen eines eingeschrĂ€nkten Wertebereiches liefert, und ordnen wir jedem Level von Nachfahren eine dieser Zahlen des Wertebereichs fest zu, also z.B. der Wurzel eine 1, den Söhnen eine 3, den Enkeln eine 2 ..., so erfolgt dadurch der Aufbau des Baumes durch EinfĂŒhrung als entsprechender Nachfahre rein zufĂ€llig, und damit mit hoher Wahrscheinlichkeit gleichmĂ€Ăig. Die Zuordnung muĂ so erfolgen, daĂ es prinzipiell mehr Enkel als Söhne, als VĂ€ter ... gibt. Das ist noch ein Problem!
Alle Ebenen sind damit mit der richtigen HĂ€ufigkeit vertreten, (eine genĂŒgend groĂe Zahl von Informationen vorausgesetzt.) Um es noch einmal zu sagen: Die Struktur des Baumes ist von der Struktur und Reihenfolge der Information abgekoppelt. Auch das Löschen von Knoten innerhalb des Baumes löscht einen zufĂ€lligen Knotentyp (Enkel, Urenkel...), so daĂ insgesamt die gleichmĂ€Ăige Struktur des Baumes erhalten bleibt. (Anmerkung: NatĂŒrlich lassen sich Arbeitsweisen finden, bei denen sich auch dieser Algorithmus nicht so gut verhĂ€lt.)
Alles hĂ€ngt von einem geeigneten Zufallszahlengenerator ab. Der Aufbau des Baumes ist also von der Reihenfolge der SchlĂŒsselbegriffe unabhĂ€ngig, die somit willkĂŒrlich, z.B. auch sortiert sein kann, er hĂ€ngt von der Reihenfolge der Zufallszahlen ab. (Anmerkung: Gerade sortierte SchlĂŒsselfolgen bereiten konventionellen Algorithmen groĂe Probleme.)
random
Der Zufallszahlenalgorithmus ist nicht beliebig wĂ€hlbar. Er muĂ bestimmte Zahlen bevorzugen. Wir wollen, wie oben schon erwĂ€hnt, mehr VĂ€ter als Söhne haben. Um genauer zu sein: es mĂŒssen doppelt soviele Söhne erzeugt werden wie VĂ€ter vorhanden sind, um ein optimales Ergebnis zu erzielen. (Verzwickt dabei ist, daĂ die Anzahl der VĂ€ter eigentlich nicht feststeht.) Je besser uns dieses gelingt, desto buschiger gestaltet sich unser Baum. Genau dieses versucht die Funktion randomLevel, und damit kommen wir zum Algorithmus.
Wir benötigen eine Initialisierung des gesamten Systems mittels der Prozedur init_skip-lists. Hier wird nur der Zufallszahlengenerator initialisiert. Es lohnt sich nicht, diese Informationen lokal je âskip listâ zu halten, da wir dann fĂŒr jede einen solchen benötigten.
Jede aktive âskip listâ benötigt die Verwaltungsstruktur listStructure, die den Startknoten, das zu jeder Zeit maximale Level (wieviele hierarchische Nachfahren-Ebenen existieren noch) und zwei Zeiger auf Funktionen erhĂ€lt. Sie ermöglichen die unabhĂ€ngige Gestaltung der âskip listâ-Funktionen, so daĂ mit den gleichen Routinen mehrere unterschiedliche BĂ€ume (z.B. zum Umsortieren) behandelt werden können.
Die Funktion *cmp_fun erhĂ€lt zwei Informationen als Argumente und muĂ entscheiden, welches von beiden âkleinerâ als das andere ist, oder ob sie âgleichâ sind. Ihre RĂŒckgabewerte entsprechen im ganzen gesehen denen der Funktion strcmp. Genaueres siehe die Header-Datei âskiplist.hâ.
Die Funktion *free_fun erhĂ€lt eine Information als Argument und entscheidet, was mit ihr nach dem Entfernen aus der âskip listâ geschehen soll.
Das Feld last enthÀlt einen Verweis auf das letzte gesuchte Element. Diese Positionsinformation wird genutzt, um auf die nachfolgenden Informationen zuzugreifen.
FĂŒr jede zu speichernde Information benötigen wir einen Zeiger auf die Information und eine sich nach dem Level richtende Anzahl von Zeigern auf die nĂ€chsten Informationen. Diese Daten sind in der Struktur nodeStructure gespeichert.
Mittels der Prozedur new_skiplist erzeugen wir eine Verwaltungsstruktur fĂŒr eine neue âskip listâ. Wir benötigen einen Startknoten, l->header, in dem wir nur die Tabelle der Zeiger auf nachfolgende Elemente der verschiedenen Levels speichern. Das Feld l->header.inf bleibt leer. Alle anderen Felder werden mit NULL initialisiert. Dieser Wert indiziert das Ende der Informationsketten.
In die âskip listâ fĂŒgen wir Informationen durch die Funktion skip_insert ein. Als Parameter werden die âskip listâ, die Information und der Boolsche Wert dp benötigt. Dieser entscheidet, ob doppelte Informationen in der Liste erlaubt sein sollen. Ist der Wert Ă€quivalent zu Null, sind sie nicht erlaubt. Die Funktion sucht, beim maximalen Level beginnend, nach Informationen, deren SchlĂŒssel kleiner als die mitgelieferte Information ist. Ăbrigens sind die SchlĂŒssel immer Teil der Information. Die Tabelle update enthĂ€lt in Stelle k die Adresse des letzten Knotens, der âkleinerâ als die neue Information ist. Falls die Information eingefĂŒgt werden soll, sind diese Werte an den entsprechenden Stellen zu modifizieren. Es wird âFALSEâ zurĂŒckgegeben, falls die Information schon vorhanden war, âTRUEâ, falls sie eingefĂŒgt werden muĂte.
skip_delete löscht eine Information und gibt âTRUEâ zurĂŒck, falls der Auftrag ausgefĂŒhrt, âFALSEâ, falls keine Information gefunden wurde. Auch hier wird als erstes nach der Information gesucht, indem man zuerst mit den am weitesten zeigenden Verweisen (âk = l->levelâ) beginnt und schlieĂlich bei den niedrigsten Verweisen endet (âk = 0â).
Mit Hilfe von skip_search ist der Zeiger l->last auf einen Teil des Suchbaumes einstellbar, von dem man dann mit skip_next auf die nachfolgenden Elemente zugreifen kann. Wurde die Information nicht gefunden, oder ist das Ende der Informationskette erreicht, wird âNULLâ zurĂŒckgeliefert. Ein nachfolgender skip_next-Aufruf beginnt am Anfang der Informationen.
Restriktionen und mögliche Verbesserungen
Nun, es gibt sie doch. Das Verfahren des Zufallszahlengenerators erlaubt eine maximale Hierarchietiefe von 16. Das bedingt, daĂ der Algorithmus im vorliegenden Fall âoptimalâ bis zu einer maximalen Anzahl von 2 hoch 16 unterschiedlichen Informationen funktioniert. DarĂŒber hinausgehende erfolgreiche EinfĂŒgungen werden zwar verkraftet, reduzieren aber die LeistungsfĂ€higkeit des Algorithmusâ.
Ist die hohe FlexibilitĂ€t nicht gefordert, lassen sich Vergleichs- und Speicherfreigabefunktionen direkt kodieren. Im vorgestellten Fall werden sie aber ĂŒber ein Unterprogramm aufgerufen, das die Geschwindigkeit des Algorithmusâ etwas mindert.Die Information lĂ€Ăt sich anstelle einer Verzeigerung auch einfach direkt in node-Structure kodieren, um so einen Effizienzgewinn zu erreichen. Dann allerdings benötigt man fĂŒr jeden aktiven Baum einen eigenen Algorithmus.
Eine weitere Optimierung besteht in der Definition eines Elements mit einem maximalen Wert, der im Falle von erlaubten Duplikaten oder nicht sicherem Auftreten niemals eingefĂŒgt werden darf. Dadurch kann die Abfrage auf âNULLâ ersatzlos gestrichen werden.
Das Programm
Das Beispielprogramm nimmt eine Textdatei als Eingabe und sortiert die darin enthaltenen Wörter (alles, was durch mindestens ein Leerzeichen getrennt ist, fĂŒhrende und abschlieĂende Kontrollzeichen werden ignoriert) in eine âskip listâ ein. Die gesamte Datei wird in den Speicher geladen, um auf zusĂ€tzliche malloc-Aufrufe zu verzichten. Ein Aufruf der Funktion âstrtokâ modifiziert destruktiv diesen Puffer und separiert so alle gefundenen Wörter.
Ein kleines Schmankerl am Rande: Das Programm schafft es, die deutschen Sonderzeichen âĂ€â, âöâ âĂŒâ und âĂâ richtig zu sortieren.
Die gewonnenen Informationen werden, um alle Wörter die mit einem âmâ beginnen, vermindert, ausgegeben. Sie könnten jetzt korrekturgelesen werden. Ein paar statistische Informationen, wieviele Knoten sich auf welchen Levels befinden, werden zur Kontrolle ebenfalls geliefert. Level 0 sollte die gröĂte Anzahl Knoten, das gröĂte Level die kleinste Anzahl liefern. Ist dies im Ganzen gesehen nicht der Fall, haben Sie entweder eine der wenigen Ausnahmen gefunden (sehr unwahrscheinlich) oder beim Abtippen einen Fehler gemacht (sehr wahrscheinlich).
Was bringtâs
- Die Routinen sind klein und schnuckelig und portabel; unter Turbo C auf dem Atari belegen sie mit allem drum und dran weniger als 800 Bytes, (vielleicht kann sie ja ein Assembler-Programmierer effizienter gestalten).
- Sie sind universell einsetzbar, da konfigurierbar. Man braucht sie nur einmal im Programm zu haben.
- Sie liefern einen zufĂ€llig âausgeglichenenâ Baum, dem auch intensive Operationen nichts anhaben können.
- Sie sind schnell. Sagâ ich ganz einfach einmal. Lediglich bei der Suchzeit mĂŒssen sie im Vergleich zu optimal programmierten, nicht-rekursiven AVL-BĂ€umen ganz leicht zurĂŒckstecken. Rekursive Verfahren schneiden schlechter ab. EinfĂŒge- und AusfĂŒgeoperationen sind jeweils wesentlich schneller. NĂ€here Informationen dazu kann man in dem unten angefĂŒhrten Artikel nachlesen.
[1] William Pugh âskip listâs: A probabilistic alternative to balanced trees; Communications of the ACM; June 1990, Vol 33, Number 6
/* Diese Routine lehnt sich an ein Beispiel von
* William Pugh an Er wurde um FunktionalitÀt
* verallgemeinert und anwenderfreundlicher
* gestaltet
*
* Klaus Elsbernd entwickelt mit TURBO C
* (c) 1991 MAXON Computer
*/
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <skiplist.h>
int randomLevel(void);
int randomsLeft;
long randomBits;
void
init_skiplists () /* --- initialize skip lists */
{
/* get a 32 bits random level number! */
randomBits = (rand()<<16)+rand();
randomsLeft = BitsInRandom/2;
} /* init_skiplists */
int
skip_cmp_default (x,y) /* compares two strings */
char *x, *y;
{
return(strcmp(x,y));
} /* skip_cmp_default */
void /* ----------- don't free the information */
skip_free_default (x)
char *x;
{
return;
} /* skip_free_default */
skip_list /* create a new skip list */
new_skiplist (cmp_fun,free_fun)
BOOLEAN (* cmp_fun)();
void (* free_fun)();
{
skip_list l;
int i;
l = (skip_list)
malloc(sizeof(struct listStructure));
l->level = 0
l->cmp_fun = (cmp_fun != NULL)
? cmp_fun : skip_cmp_default;
l->free_fun = (free_fun != NULL)
? free_fun skip_free_default;
l->header = newNodeOfLevel(MaxNumberOfLevels);
l->last = NULL;
l->no = 0; /* no of entries in the skiplist */
/* If you need information about used memory,
* uncomment the following statement
l->memory = sizeof(struct listStructure)
+ sizeof(struct nodeStructure)
+ (MaxNumberOfLevels)*sizeof(node);
*/
for (i = 0; i < MaxNumberOfLevels; i++)
l->header->forward[i] = NULL;
return(1);
} /* new_skiplist */
void
free_skiplist (l)/* free an allocated skiplist */
skip_list l;
{
register node p, q;
p = l->header;
do {
q = p->forward[0];
/* free the information */
(l->free_fun)(p->inf);
free(p); /* free node storage */
p = q;
} while (p != NULL);
free(l->header); /* free header storage */
free(l); /* free list storage */
} /* free_skiplist */
int
randomLevel () /* --------- get random level */
{
register int level = 0;
register long b;
/* While 2 consecutive bits of a random number
* are 1= 0 add 1 to level;
* It will be very unlikely, that many sequences
* will result in high levels. */
do {
b = randomBits & 3;
if (!b) level++;
randomBits >>= 2;
if (--randomsLeft == 0) {
randomBits = (rand()<<16)+rand();
randomsLeft = BitsInRandom/2;
}
} while (!b);
return(level > MaxLevel ? MaxLevel : level);
} /* randomLevel */
BOOLEAN
skip_msert (1, inf, dp) /* insert information */
register skip_list l;
register Information *inf;
BOOLEAN dp,
{
register int k;
register node p, q;
register int cmp;
node update[MaxNumberOfLevels];
p = l->header;
/* Search first the skip list elements within
* level k using the comparison function
* cmp_fun. Than decrement k down to level 0
*/
k = l->level;
do {
while (q = p->forward[k],
q != NULL
&& (cmp = l->cmp_fun(q->inf,inf))
< 0) {
p = q;
}
update[k] = p;
} while (--k >= 0);
/* If the key is already there and duplicates
* are not allowed, than update the value
*/
if (!dp && q != NULL && cmp == 0) {
/* the last information will be stored */
q->inf = inf;
return(FALSE);
}
l->no++; /* count number of nodes */
/* Generate a new random level for the new key
* and initialize it with the apropnate
* pointers
*/
k = randomLevel();
/* Is the new level
* greater than the old skiplist level? */
if (k > l->level) {
k = ++l->level;
update[k] = l->header;
}
q = newNodeOfLevel(k);
/* If you need information about used memory,
* uncomment the following statement
l->memory += sizeof(struct nodeStructure)
+ (k)*sizeof(node);
*/
q->inf = inf;
do { /* insert the new element */
p = update[k]; /* save old pointers */
q->forward[k] = p->forward[k];
p->forward[k] = q; /* insert the new one */
} while (--k >= 0) ;
return(TRUE);
} /* skip_insert */
BOOLEAN
skip_delete (l,inf) /* delete information */
register skip_list l;
register Information *inf;
{
register int k, m;
register int cmp;
node update[MaxNumberOfLevels],
register node p, q;
p = l->header;
k = m = l->level;
do { /* find the information */
while (q = p->forward[k],
q != NULL
&& (cmp = l->cmp_fun(q->inf,inf)) < 0)
p = q;
update[k] = p;
} while (â-k >= 0);
if (q != NULL && cmp == 0) { /* found? */
/* update all pointers pointing to the
* information to be deleted */
for (k = 0;
k <= m
&& (p = update[k])->forward[k] = q;
k++) {
p->forward[k] = q->forward[k];
}
/* decrease the maximal skip list level,
* if necessary */
while (l->header->forward[m] == NULL && m > 0)
mâ-;
l->level = m;
l->noâ-;
(l->free_fun)(q->inf);
free(q);
/* If you need information about used memory,
* uncomment the following statement
l->memory -= sizeof(struct nodeStructure)
+ (k)*sizeof(node);
*/
return (TRUE) ; /* true if key not found */
}
else
return(FALSE);/* false if key not found */
} /* skip_delete */
node
skip_search (1, inf) /* ---------- search a key */
register skip_list l;
register Information *inf;
{
register int k;
register int cmp;
register node p, q;
p = l->header;
k = l->level;
do {
while (q = p->forward[k],
q != NULL
&& (cmp = l->cmp_fun(q->inf,inf)) < 0)
p = q;
} while (â-k >= 0);
l->last = q; /* remember last searched cell */
if (q != NULL && cmp != 0)
return(NULL);
return(q);
} /* skip_search */
node
skip_next (l) /* search for next information */
register skip_list 1,
{
if (l->last = NULL)
l->last = l->header;
l->last = l->last->forward[0];
return(l->last);
} /* skip_next */
/* include file for skip-lists *
*
* Klaus Elsbernd TURBO C
* (c) 1991 MAXON Computer
*/
#ifndef Information
/* no already defined by the user */
#define Information char
#endif
#define FALSE 0
#define TRUE 1
#define BOOLEAN int
#define BitsInRandom 31
#define allowDuplicates
#define MaxNumberOfLevels 16
#define MaxLevel (MaxNumberOfLevels-1)
#define newNodeOfLevel(1) (node)malloc(sizeof(struct nodeStructure) + (1)*sizeof(node))
/* 2 structures needed */
typedef struct nodeStructure *node;
typedef struct nodeStructure {
Information *inf;
/* variable sized array of forward pointer */
node forward[1];
};
typedef struct listStructure {
/* Maximum level of the list (1 more, than
* the number of levels in the list) */
int level;
unsigned int no; /* number of nodes in list */
/* pointer to the firstheader */
/* if you need information about used Memory,
* uncomment the following statement
unsigned long memory.
*/
struct nodeStructure *header;
struct nodeStructure *last;
/* pointer to comparasion function with
* two arguments
* cmp_fun returns
* 0 iff argument 1 = argument 2
* 1 iff argument 1 > argument 2
* -1 iff argument 1 < argument 2
*/
int (* cmp_fun)
(Information *x,Information *y);
void (* free_fun)(Information *inf);
} *skip_list;
/* prototypes: */
void init_skiplists (void) ;
skip_list new_skiplist(BOOLEAN (* cmp_fun)(), void (* free_fun)());
void free_skiplist(skip_list l);
int skip_cmp_default(Information *x, Information *y);
void skip_free_default (Information *inf) ;
BOOLEAN skip_insert(skip_list l,
Information *inf,
BOOLEAN dp),
skip_delete(skip_list l,
Information *inf);
node skip_search(skip_list l,
Information *inf),
skip_next(skip_list l);
/* externals: */
extern int skip_cmp_default();
extern void skip_free_default();
extern BOOLEAN skip_insert(), skip_delete();
extern node skip_search(), skip_next();
extern void init_skiplists(), free_skiplist();
extern skip_list new_skiplist();
/* This program read a character file
* and inserts all words, separated by spaces
* into a skip list;
* Then all words are printed on a new line
*
* Klaus Elsbernd TURBO C
* (c) 1991 MAXON Computer
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
/* Information is a string */
#define Information char
#include <skiplist.h>
/* define german characters: */
#define ae (unsigned char)'\204'
#define Ae (unsigned char)'\216'
#define oe (unsigned char)'\224'
#define Oe (unsigned char)'\231'
#define ue (unsigned char)'\201'
#define Ue (unsigned char)'\232'
#define ss (unsigned char)'\236'
void sortcharupcase(unsigned char * pointer);
int eqlchupper(unsigned char ch1,
unsigned char ch2);
int cmp_str(char *x, char *y);
void /* â prepares character for comparing */
sortcharupcase (ptr)
unsigned char *ptr;
{
unsigned char c;
if ((c = *ptr) >= 'a') {
if (c <= 'z')
*ptr -= 'a'-'A';
else
*ptr = (c == ae || c == Ae)
? 'A' :
(c == oe || c == Oe) ? 'O' :
(c == ue || c == Ue) ? 'U' :
(c == ss) ? 'S' : *ptr;
}
} /* sortcharupcase */
int /* compares 2 characters, if they are eql */
eqlchupper (ch1,ch2)
unsigned char ch1, ch2;
{
sortcharupcase(&ch1);
sortcharupcase(&ch2);
return(ch1 - ch2);
} /* eqlchupper */
int
cmp_str (x, y) /* --------- compares two strings */
register char *x, *y,
{
/* compare strings ignoring
* upper/lower-case differences */
while (*x && *y &&
(!(*x - *y) || !eqlchupper(*x,*y))) {
x++; y++;}
return(eqlchupper(*x,*y));
} /* cmp_str */
void
free_str (x) /* --------- free an information */
char *x;
{
return;
} /* free_str */
int
main (argc,argv)
int argc;
char *argv[],
{
skip_list l;
FILE *file;
long size;
char *buffer, *str;
register node p, q;
int i, k;
long no;
if ((argc != 2) /* argument given? */
|| (file = fopen(argv[1],"r")) == NULL) {
printf("parameter <existing file> required\n");
return(-1);
}
init_skiplists(); /* initialize skip list */
l = new_skiplist(cmp_str,skip_free_default);
/* determine the length of the file */
if (fseek(file,01,SEEK_END) != 0) {
printf("error at positioning at end\n");
return(-1);
}
size = ftell(file); /* size of the file */
fseek(file,01,SEEK_SET);
/* allocate enough buffer for the file */
str = buffer =(char *)malloc((size_t)(size+1));
/* read all lines, and separate them by ' ' */
while (fgets(str,(int)size,file) != NULL) {
str += strlen(str)-1;
*str++ = ' ';
}
fclose(file);
/* tokenize the input file and put the token
* in the tree */
no = 0; /* number of insert operations */
for (str = strtok(buffer," ");
str != NULL;
str = strtok(NULL," ")) {
/* skip most of non literal characters */
for (; *str != '\0'
&& ((unsigned)(*str) < 'A');
str++);
for (i = (int)strlen(str)-1;
i > 0
&& ((unsigned)(*(str+i)) < 'A');
i--)
*(str+i) = 0;
no++;
skip_insert(1, str, FALSE);
}
/* provide same statistic information */
for (k = l->level; k >= 0; kâ-) {
p = l->header;
i = 0;
while (q = p->forward[k], q != NULL) {
p = q;
i++;
}
printf("No of nodes in level %d: %d\n",k,i);
}
if (i != l->no)
printf("illegal number of nodes\n");
printf("Number of inserted strings %ld\n",no);
/* delete all strings
beginning with an 'm' or 'M' */
if ((p = skip_search(l,"M")) == NULL)
p = skip_next(l);
for (; p != NULL
&& cmp_str(p->inf,"N") < 0;
p = q) {
q = skip_next(l);
skip_delete(l,p->inf);
}
/* print the contents of the skip list
* in sorted order */
p = l->header->forward[0]; /* first element */
while (p != NULL) {
printf("%s\n",p->inf);
p = p->forward[0];
}
free_skiplist(1); /* obvious */
return(0);
) /* main */