Datenorganisation 4. Bäume 4.1 Grundlagen 4.1.1 Grundbegriffe und Definitionen Bäume sind eine Struktur zur Speicherung von (meist ganzahligen) Schlüsseln. Die Schlüssel werden so gespeichert, daß sie sich in einem einfachen und effizienten Verfahren wiederfinden lassen. Neben dem Suchen sind üblicherweise das Einfügen eines neuen Knoten (mit gegebenem Schlüssel), das Entfernen eines Knoten (mit gegebenem Schlüssel), das Durchlaufen aller Knoten eines Baums in bestimmter Reihenfolge erklärte Operationen. Weitere wichtige Verfahren sind: - Das Konstruieren eines Baums mit bestimmten Eigenschaften - Das Aufspalten eines Baums in mehrere Teilbäme - Das Zusammenfügen mehrere Bäume zu einem Baum Definitionen Eine Datenstruktur heißt "t-ärer" Baum, wenn zu jedem Element höchstens t Nachfolger (t = 2,3,.... ) festgelegt sind. "t" bestimmt die Ordnung des Baumes (z.B. "t = 2": Binärbaum, "t = 3": Ternärbaum). Die Elemente eines Baumes sind die Knoten (K), die Verbindungen zwischen den Knoten sind die Kanten (R). Sie geben die Beziehung (Relation) zwischen den Knotenelementen an. Eine Datenstruktur D = (K,R) ist ein Baum, wenn R aus einer Beziehung besteht, die die folgenden 3 Bedingungen erfült: 1. Es gibt genau einen Ausgangsknoten (, das ist die Wurzel des Baums). 2. Jeder Knoten (mit Ausnahme der Wurzel) hat genau einen Vorgänger 3. Der Weg von der Wurzel zu jedem Knoten ist ein Pfad, d.h.: Für jeden von der Wurzel verschiedenen Knoten gibt es genau eine Folge von Knoten k1, k2, ... , kn (n >= 2), bei der ki der Nachfolger von ki-1 ist. Die größte vorkommende Pfadlänge ist die Höhe eines Baums. Knoten, die keinen Nachfolger haben, sind die Blätter. Knoten mit weniger als t Nachfolger sind die Randknoten. Blätter gehören deshalb mit zum Rand. Haben alle Blattknoten eines vollständigen Baums die gleiche Pfadlänge, so heißt der Baum voll. Quasivoller Baum: Die Pfadlängen der Randknoten unterscheiden sich höchstens um 1. Bei ihm ist nur die unterste Schicht nicht voll besetzt. Linksvoller Baum: Blätter auf der untersten Schicht sind linksbündig dicht. Geordneter Baum: Ein Baum der Ordnung t ist geordnet, wenn für jeden Nachfolger k' von k festgelegt ist, ob k' der 1., 2., ... , t. Nachfolger von k ist. Dabei handelt es sich um eine Teilordnung, die jeweils die Söhne eines Knoten vollständig ordnet. 1 Datenorganisation Speicherung von Bäumen Im allg. wird die der Baumstruktur zugrundeliegende Relation gekettet gespeichert, d.h.: Jeder Knoten zeigt über einen sog. Relationenteil (mit t Komponenten) auf seine Nachfolger. Die Verwendung eines Anfangszeigers (, der auf die Wurzel des Baums zeigt,) ist zweckmäßig. 4.1.2 Darstellung von Bäumen In der Regel erfolgt eine grafische Darstellung: Ein Nachfolgerknoten k' von k wird unterhalb des Knoten k gezeichnet. Bei der Verbindung der Knotenelemente reicht deshalb eine ungerichte Linie (Kante). Abb.: Bei geordneten Bäumen werden die Nachfolger eines Knoten k in der Reihenfolge "1. Nachfolger", "2. Nachfolger", .... von links nach rechts angeordnet. Ein Baum ist demnach ein gerichteter Graf mit der speziellen Eigenschaft: Jeder Knoten (Sohnknoten) hat bis auf einen (Wurzelknoten) genau einen Vorgänger (Vaterknoten). 2 Datenorganisation Ebene 1 linker Teilbaum von k Ebene 2 k Ebene 3 Weg, Pfad Ebene 4 Randknoten oder Blätter Abb.: 4.1.3 Berechnungsgrundlagen Die Zahl der Knoten in einem Baum ist N. Ein voller Baum der Höhe h enthält: h (1) N = ∑ t i −1 = i =1 th −1 t −1 Bspw. enthält ein Binärbaum N = 2 h − 1 Knoten. Das ergibt für den Binärbaum der Höhe 3 sieben Knoten. Unter der Pfadsumme Z versteht man die Anzahl der Knoten, die auf den unterschiedlichen Pfaden im vollen t-ären Baum auftreten können: h (2) Z = ∑ i ⋅ t i −1 i =1 Die Summe kann durch folgende Formel ersetzt werden: 3 Datenorganisation Z= h⋅th th −1 − t − 1 ( t − 1) 2 t und h können aus (1) bestimmt werden: t h = N ⋅ ( t − 1) + 1 h = log t ( N ⋅ ( t − 1) + 1) Mit Gleichung (1) ergibt sich somit für Z Z= log t ( N ⋅ ( t − 1) + 1) ⋅ ( N ⋅ ( t − 1) + 1) N ⋅ ( t − 1) ( N ⋅ ( t − 1) + 1) ⋅ log t ( N ⋅ ( t − 1) + 1) − N − = t −1 t −1 ( t − 1) 2 Für t = 2 ergibt sich damit: Z = h ⋅ 2 h − (2 h − 1) bzw. Z = ( N + 1) ⋅ ld ( N + 1) − N Die mittlere Pfadlänge ist dann: (3) Z mit = Z ( N ⋅ ( t − 1) + 1) ⋅ log t ( N ⋅ ( t − 1) + 1) − N ( N ⋅ ( t − 1) + 1) ⋅ log t ( N ⋅ ( t − 1) + 1) 1 = = − N N ⋅ ( t − 1) N ⋅ ( t − 1) t −1 Für t = 2 ist (4) Z mit = Z N +1 = ⋅ ld ( N + 1) − 1 N N Die Formeln unter (3) und (4) ergeben den mittleren Suchaufwand bei gleichhäufigem Aufsuchen der Elemente. Ist dies nicht der Fall, so ordnet man den Elementen die relativen Gewichte gi (i = 1, 2, 3, ... , N) bzw. die Aufsuchwahrscheinlichkeiten zu: pi = N gi , G = ∑ gi G i =1 N Man kann eine gewichtete Pfadsumme Zg = ∑ gi ⋅ hi i =1 Suchaufwand ( Z g ) mit = Zg G N = ∑ p i ⋅ h i berechnen. i =1 4 bzw. einen mitlleren Datenorganisation 4.1.4 Klassifizierung von Bäumen Wegen der großen Bedeutung, die binäre Bäume besitzen, ist es zweckmäßig in Binär- und t-äre Bäume zu unterteilen. Bäume werden oft verwendet, um eine Menge von Daten festzulegen, deren Elemente nach einem Schlüssel wiederzufinden sind (Suchbäume). Die Art, nach der beim Suchen in den Baumknoten eine Auswahl unter den Nachfolgern getroffen wird, ergibt ein weiteres Unterscheidungsmerkmal für Bäume. Intervallbäume In den Knoten eines Baumes befinden sich Daten, mit denen immer feinere Teilintervalle ausgesondert werden. Bsp.: Binärer Suchbaum Die Schlüssel sind nach folgendem System angeordnet. Neu ankommende Elemente werden nach der Regel "falls kleiner" nach links bzw. "falls größer" nach rechts abgelegt. 40 30 50 20 11 39 24 37 60 44 40 41 45 62 65 Es kann dann leicht festgestellt werden, in welchem Teilbereich ein Schlüsselwort vorkommt. Selektorbäume (Entscheidungsbäume) Der Suchweg ist hier durch eine Reihe von Eigenschaften bestimmt. Beim Binärbaum ist das bspw. eine Folge von 2 Entscheidungsmöglichkeiten. Solche Entscheidungsmöglichkeiten können folgendermaßen codiert sein: - 0 : Entscheidung für den linken Nachfolger - 1 : Entscheidung für den rechten Nachfolger Die Folge von Entscheidungen gibt dann ein binäres Codewort an. Dieses Codewort kann mit einem Schlüssel bzw. mit einem Schlüsselteil übereinstimmen. Bsp.: "Knotenorientierter binärer Selektorbaum" Folgende Schlüsselfolge wird zum Erstellen des Baums herangezogen: 5 Datenorganisation 1710 3810 6310 1910 3210 2910 4410 2610 5310 = = = = = = = = = 0 1 1 0 1 0 1 0 1 1 0 1 1 0 1 0 1 1 0 0 1 0 0 1 1 1 0 0 1 1 0 0 1 1 0 1 0 1 1 1 0 0 0 1 1 12 02 12 12 02 12 02 02 12 Der zugehörige Binärbaum besitzt dann folgende Gestalt: 17 19 0_ 1_ 38 39 01_ 32 63 10_ 11_ 101_ 40 011_ 44 53 110_ In den Knoten dient demnach der Wertebereich einer Teileigenschaft zur Auswahl der nächsten Untergruppe. Knotenorientierte und blattorientierte Bäume Zur Unterscheidung von Bäumen kann auf die Aufbewahrungsstelle der Daten zurückgegriffen werden: 1. Knotenorientierte Bäume Daten befinden sich hier in allen Baumknoten 2. Blattorientierte Bäume Daten befinden sich nur in den Blattknoten Optimierte Bäume Man unterscheidet statisch und dynamisch optimierte Bäume. In beiden Fällen sollen entartete Bäume (schiefe Bäume, Äste werden zu linearen Listen) vermieden werden. Statische Optimierung bedeutet: Der Baum wird neu (oder wieder neu) aufgebaut. Optimalität ist auf die Suchoperation bezogen. Es interessiert dabei das Aussehen des Baums, wenn dieser vor Gebrauch optimiert werden kann. Bei der dynamischen Optimierung wird der Baum während des Betriebs (bei jedem Ein- und Ausfügen) optimiert. Ziel ist also hier: Eine günstige Speicherstruktur zu erhalten. Diese Aufgabe kann im allg. nicht vollständig gelöst werden, eine Teiloptimierung (lokale Optimierung) reicht häufig aus. 6 Datenorganisation Werden die Operationen "Einfügen", "Löschen" und "Suchen" ohne besondere Einschränkungen oder Zusätze angewendet, so spricht man von freien Bäumen. Strukturbäume Sie dienen zur Darstellung und Speicherung hierarchischer Zusammenhänge. Bsp.: "Darstellung eines arithmetischen Ausdrucks" Operationen in einem arithmetischen Ausdruck sind zweiwertig (, die einwertige Operation "Minus" kann als Vorzeichen dem Operanden direkt zugeordnet werden). Zu jeder Operation gehören demnach 2 Operanden. Somit bietet sich die Verwendung eines binären Baumes an. Für den arithmetischen Ausdruck (A + B / C)∗ ( D − E∗ F) ergibt sich dann folgende Baumdarstellung: * + - A / B D * E C Abb.: 7 F Datenorganisation 4.2 Freie Binäre Intervallbäume 4.2.1 Ordnungsrelation und Darstellung Freie Bäume sind durch folgende Ordnungsrelation bestimmt: In jedem Knoten eines knotenorientierten, geordneten Binärbaums gilt: Alle Schlüssel im rechten (linken) Unterbaum sind größer (kleiner) als der Schlüssel im Knoten selbst. Mit Hilfe dieser Ordnungsrelation erstellte Bäume dienen zum Zugriff auf Datenbestände (Aufsuchen eines Datenelements). Die Daten sind die Knoten (Datensätze, -segmente, -elemente). Die Kanten des Zugriffsbaums sind Zeiger auf weitere Datenknoten (Nachfolger). Dateninformation Schluessel Datenteil Knotenzeiger LINKS RECHTS Zeiger zum linken Sohn Zeiger zum rechten Sohn Abb.: Das Aufsuchen eines Elements im Zugriffsbaum geht vom Wurzelknoten über einen Kantenzug (d.i. eine Reihe von Zwischenknoten) zum gesuchten Datenelement. Bei jedem Zwischenknoten auf diesem Kantenzug findet ein Entscheidungsprozeß über die folgenden Vergleiche statt: 1. Die beiden Schlüssel sind gleich: Das Element ist damit gefunden 2. Der gesuchte Schlüssel ist kleiner: Das gesuchte Element kann sich dann nur im linken Unterbaum befinden 3. Der gesuchte Schlüssel ist größer: Das gesuchte Element kann sich nur im rechten Unterbaum befinden. Das Verfahren wird solange wiederholt, bis das gesuchte (Schlüssel-) Element gefunden ist bzw. feststeht, daß es in dem vorliegenden Datenbestand nicht vorhanden ist. Struktur und Wachstum binärer Bäume sind durch die Ordnungsrelation bestimmt: Aufgabe: Betrachte die 3 Schlüssel 1, 2, 3. Diese 3 Schlüssel können durch verschieden angeordnete Folgen bei der Eingabe unterschiedliche binäre Bäume erzeugen. Stellen Sie alle Bäume, die aus unterschiedlichen Eingaben der 3 Schlüssel resultieren, dar! 8 Datenorganisation 1, 2, 3 1, 3, 2 2, 1, 3 1 1 2 3 2 3 1 2 2, 3, 1 3, 1, 2 3, 2, 1 2 3 3 1 3 2 1 1 Es gibt also: Sechs unterschiedliche Eingabefolgen und somit 6 unterschiedliche Bäume. Allgemein können n Elemente zu n! verschiedenen Anordnungen zusammengestellt werden. Suchaufwand Der mittlere Suchaufwand für einen vollen Baum beträgt Z mit = N +1 ⋅ ld ( N + 1) − 1 N Zur Bestimmung des Suchaufwands stellt man sich vor, daß ein Baum aus dem leeren Baum durch sukzessives Einfuegen der N Schlüssel entstanden ist. Daraus resultieren N! Permutationen der N Schlüssel. Über all diese Baumstrukturen ist der Mittelwert zu bilden, um schließlich den Suchaufwand festlegen zu können. Aus den Schlüsselwerten 1, 2, ... , N interessiert zunächst das Element k, das die Wurzel bildet. k Hier gibt es: (k-1)! Unterbäume Schlüsseln 1..N Hier gibt es: (N-k)! Unterbäume mit den Schlüsseln k+1, ... , N 9 Datenorganisation Der mittlere Suchaufwand im gesamten Baum ist: ZN = 1 ( N + Z k −1 ⋅ ( k − 1) + Z N − k ⋅ ( N − k )) N Zk-1: mittlerer Suchaufwand im linken Unterbaum ZN-k: mittlerer Suchaufwand im rechten Unterbaum Zusätzlich zu diesen Aufwendungen entsteht ein Aufwand für das Einfügen der beiden Teilbäume an die Wurzel. Das geschieht (N-1)-mal. Zusammen mit dem dem Suchschritt selbst ergibt das N-mal. Der angegebene Suchaufwand gilt nur für die Wurzel mit dem Schlüssel k. Da alle Werte für k gleichwahrscheinlich sind, gilt allgemein: ZN ( k) = 1 N 2 N 1 bzw. ⋅ ( N + Z ⋅ ( k − ) + Z ⋅ ( N − k )) Z = 1 + ⋅ ∑ (Z k −1 ⋅ ( k − 1) ∑ 1 k − N − k N N 2 k =1 N 2 k =1 N −1 2 ⋅ ∑ (Z k −1 ⋅ ( k − 1) ( N − 1) 2 k =1 N −1 2 N 2 = 2 ⋅ ∑ Z k −1 ⋅ ( k − 1) − ⋅ ∑ Z k −1 ⋅ ( k − 1) ( N − 1) 2 k −1 N k =1 bzw. für N - 1: Z N −1 = 1 + Z N − Z N −1 Es läßt sich daraus ableiten: Mit der YN = YN −1 + Ersatzfunktion 2⋅N −1 N N −1 ⋅ Z N +1 + ⋅ ZN = N +1 N N ⋅ ( N + 1) YN = N ⋅ ZN N +1 folgt die Rekursionsformel: N N 2⋅N −1 2⋅i −1 1 N bzw. nach Auflösung1 YN = ∑ = 2 ⋅ ∑ − 3⋅ N ⋅ ( N + 1) N +1 i = 1 i ⋅ ( i + 1) i =1 i Einsetzen ergibt: ZN = 2 ⋅ N +1 N +1 ⋅ HS N − 3 = 2 ⋅ ⋅ ( HS N +1 − 1) − 1 N N N "HS" ist die harmonische Summe: HS N = ∑ i =1 1 N Sie läßt sich näherungsweise mit ln( N ) + 0.577 ⋅ (ln( N )) = 0.693 ⋅ ld ( N ) . Damit ergibt sich schließlich: Z mit = 14 . ⋅ ld ( N + 1) − 2 Darstellung Jeder geordnete binäre Baum ist eindeutig durch folgende Angaben bestimmt: 1. Angabe der Wurzel 1 vgl. Wettstein, H.: Systemprogrammierung, 2. Auflage, S.291 10 Datenorganisation 2. Für jede Kante Angabe des linken Teilbaums ( falls vorhanden) sowie des rechten Teilbaums (falls vorhanden) Die Angabe für die Verzweigungen befinden sich in den Baumknoten, die die zentrale Konstruktionseinheit für den Aufbau binärerer Bäume sind. 1. Die Klassenschablone „Baumknoten“ in C++2 // Schnittstellenbeschreibung // Die Klasse binaerer Suchbaum binSBaum benutzt die Klasse baumKnoten template <class T> class binSBaum; // Deklaration eines Binaerbaumknotens fuer einen binaeren Baum template <class T> class baumKnoten { protected: // zeigt auf die linken und rechten Nachfolger des Knoten baumKnoten<T> *links; baumKnoten<T> *rechts; public: // Das oeffentlich zugaenglich Datenelement "daten" T daten; // Konstruktor baumKnoten (const T& merkmal, baumKnoten<T> *lzgr = NULL, baumKnoten<T> *rzgr = NULL); // virtueller Destruktor virtual ~baumKnoten(void); // Zugriffsmethoden auf Zeigerfelder baumKnoten<T>* holeLinks(void) const; baumKnoten<T>* holeRechts(void) const; // Die Klasse binSBaum benoetigt den Zugriff auf // "links" und "rechts" friend class binSBaum<T>; }; // Schnittstellenfunktionen // Konstruktor: Initialisiert "daten" und die Zeigerfelder // Der Zeiger NULL verweist auf einen leeren Baum template <class T> baumKnoten<T>::baumKnoten (const T& merkmal, baumKnoten<T> *lzgr, baumKnoten<T> *rzgr): daten(merkmal), links(lzgr), rechts(rzgr) {} // Die Methode holeLinks ermoeglicht den Zugriff auf den linken // Nachfolger template <class T> baumKnoten<T>* baumKnoten<T>::holeLinks(void) const { // Rueckgabe des Werts vom privaten Datenelement links return links; } // Die Methode "holeRechts" erlaubt dem Benutzer den Zugriff auf den // rechten Nachfoger template <class T> baumKnoten<T>* baumKnoten<T>::holeRechts(void) const { // Rueckgabe des Werts vom privaten Datenelement rechts return rechts; } 2 vgl. baumkno.h 11 Datenorganisation // Destruktor: tut eigentlich nichts template <class T> baumKnoten<T>::~baumKnoten(void) {} 2. Baumknotendarstellung in Java import java.util.*; // Elementarer Knoten eines binaeren Baums, der nicht ausgeglichen ist // Der Zugriff auf diese Klasse ist nur innerhalb eines Verzeichnisses // bzw. Pakets moeglich class BinaerBaumknoten { // Instanzvariable protected BinaerBaumknoten links; protected BinaerBaumknoten rechts; public Comparable daten; // linker Teilbaum // rechter Teilbaum // Dateninhalt der Knoten // Konstruktor public BinaerBaumknoten(Comparable datenElement) { this(datenElement, null, null ); } public BinaerBaumknoten(Comparable datenElement, BinaerBaumknoten l, BinaerBaumknoten r) { daten = datenElement; links = l; rechts = r; } public BinaerBaumknoten getLinks() { return links; } public BinaerBaumknoten getRechts() { return rechts; } } 4.2.2 Operationen 1. Generieren eines Suchbaums Bsp.: Gestalt des durch die Prozedur ERSTBAUM erstellten Binärbaums nach der Eingabe der Schlüsselfolge (12, 7, 15, 5, 8, 13, 2, 6, 14). 12 Datenorganisation Schlüssel LINKS 12 RECHTS 7 5 2 15 8 13 6 14 Abb.: a) Erzeugen eines Binärbaumknotens bzw. eines binären Baums in C++ Zum Erzeugen eines Binärbaumknotens kann folgende Funktionsschablone herangezogen werden: template <class T> baumKnoten<T>* erzeugebaumKnoten(T merkmal, baumKnoten<T>* lzgr = NULL, baumKnoten<T>* rzgr = NULL) { baumKnoten<T> *z; // Erzeugen eines neuen Knoten z = new baumKnoten<T>(merkmal,lzgr,rzgr); if (z == NULL) { cerr << "Kein Speicherplatz!\n"; exit(1); } return z; // Rueckgabe des Zeigers } Der durch den Baumknoten belegte Funktionsschablone freigegeben werden: template <class T> void gibKnotenFrei(baumKnoten<T>* z) { delete z; } 13 Speicherplatz kann über folgende Datenorganisation Der folgende Hauptprogrammabschnitt erzeugt einen binären Baum folgende Gestalt: ‘A’ ‘B’ ‘C’ ‘D’ ‘E’ Abb:: void main(void) { baumKnoten<char> *a, *b, *c, *d, *e; d = new baumKnoten<char>('D'); e = new baumKnoten<char>('E'); b = new baumKnoten<char>('B',NULL,d); c = new baumKnoten<char>('C',e); a = new baumKnoten<char>('A',b,c); } 14 Datenorganisation b) Erzeugen eines Binärbaumknotens bzw. eines binären Baums in Java class StringBinaerBaumknoten { private String sK; protected StringBinaerBaumknoten links, rechts; public StringBinaerBaumknoten (String s) { links = rechts = null; sK = s; } public void insert (String s) // Fuege s korrekt ein. { if (s.compareTo(sK) > 0) // dann rechts { if (rechts==null) rechts = new StringBinaerBaumknoten(s); else rechts.insert(s); } else // sonst links { if (links==null) links=new StringBinaerBaumknoten(s); else links.insert(s); } } public String getString () { return sK; } public StringBinaerBaumknoten getLinks () { return links; } public StringBinaerBaumknoten getRechts () { return rechts; } } public class TestStringBinaerBaumKnoten { public static void main (String args[]) { StringBinaerBaumknoten baum=null; for (int i = 0; i < 20; i++) // 20 Zusfallsstrings speichern { String s = "Zufallszahl " + (int)(Math.random() * 100); if (baum == null) baum = new StringBinaerBaumknoten(s); else baum.insert(s); } print(baum); // Sortiert wieder ausdrucken } public static void print (StringBinaerBaumknoten baum) // Rekursive Druckfunktion { if (baum == null) return; print(baum.getLinks()); System.out.println(baum.getString()); print(baum.getRechts()); } } 2. Suchen und Einfügen 15 Datenorganisation Vorstellung zur Lösung: 1. Suche nach dem Schlüsselwert 2. Falls vorhanden kein Einfügen 3. Bei erfolgloser Suche Einfügen als Sohn des erreichten Blatts a) Implementierung in C++ Das „Einfügen“3 ist eine Methode in der Klassenschablone für einen binären Suchbaum binSBaum. Zweckmäßigerweise stellt diese Klasse Datenelemente unter protected zur Verfügung. #include "baumkno.h" // Schnittstellenbeschreibung template <class T> class binSBaum { protected: // Zeiger auf den Wurzelknoten und den Knoten, auf den am // haeufigsten zugegriffen wird baumKnoten<T> *wurzel; baumKnoten<T> *aktuell; // Anzahl Knoten im Baum int groesse; // Speicherzuweisung / Speicherfreigabe // Zuweisen eines neuen Baumknoten mit Rueckgabe // des zugehoerigen Zeigerwerts baumKnoten<T> *holeBaumKnoten(const T& merkmal, baumKnoten<T> *lzgr,baumKnoten<T> *rzgr) { baumKnoten<T> *z; // Datenfeld ubd die beiden Zeiger werden initialisiert z = new baumKnoten<T> (merkmal, lzgr, rzgr); if (z == NULL) { cerr << "Speicherbelegungsfehler!\n"; exit(1); } return z; } // gib den Speicherplatz frei, der von einem Baumknoten belegt wird void freigabeKnoten(baumKnoten<T> *z) // wird vom Kopierkonstruktor und Zuweisungsoperator benutzt { delete z; } // Kopiere Baum b und speichere ihn im aktuellen Objekt ab baumKnoten<T> *kopiereBaum(baumKnoten<T> *b) // wird vom Destruktor, Zuweisungsoperator und bereinigeListe benutzt { baumKnoten<T> *neulzgr, *neurzgr, *neuerKnoten; // Falls der Baum leer ist, Rueckgabe von NULL if (b == NULL) return NULL; // Kopiere den linken Zweig von der Baumwurzel b und weise seine // Wurzel neulzgr zu if (b->links != NULL) neulzgr = kopiereBaum(b->links); else neulzgr = NULL; // Kopiere den rechten Zweig von der Baumwurzel b und weise seine // Wurzel neurzgr zu if (b->rechts != NULL) neurzgr = kopiereBaum(b->rechts); else neurzgr = NULL; 3 vgl. bsbaum.h 16 Datenorganisation // Weise Speicherplatz fuer den aktuellen Knoten zu und weise seinen // Datenelementen Wert und Zeiger seiner Teilbaeume zu neuerKnoten = holeBaumKnoten(b->daten, neulzgr, neurzgr); return neuerKnoten; } // Loesche den Baum, der durch im aktuellen Objekt gespeichert ist void loescheBaum(baumKnoten<T> *b) // Lokalisiere einen Knoten mit dem Datenelementwert von merkmal // und seinen Vorgaenger (eltern) im Baum { // falls der aktuelle Wurzelknoten nicht NULL ist, loesche seinen // linken Teilbaum, seinen rechten Teilbaum und dann den Knoten selbst if (b != NULL) { loescheBaum(b->links); loescheBaum(b->rechts); freigabeKnoten(b); } } // Suche nach dem Datum "merkmal" im Baum. Falls gefunden, Rueckgabe // der zugehoerigen Knotenadresse; andernfalls NULL baumKnoten<T> *findeKnoten(const T& merkmal, baumKnoten<T>* & eltern) const { // Durchlaufe b. Startpunkt ist die Wurzel baumKnoten<T> *b = wurzel; // Die "eltern" der Wurzel sind NULL eltern = NULL; // Terminiere bei einen leeren Teilbaum while(b != NULL) { // Halt, wenn es passt if (merkmal == b->daten) break; else { // aktualisiere den "eltern"-Zeiger und gehe nach rechts // bzw. nach links eltern = b; if (merkmal < b->daten) b = b->links; else b = b->rechts; } } // Rueckgabe des Zeigers auf den Knoten; NULL, falls nicht gefunden return b; } public: // Konstruktoren, Destruktoren binSBaum(void); binSBaum(const binSBaum<T>& baum); ~binSBaum(void); // Zuweisungsoperator binSBaum<T>& operator= (const binSBaum<T>& rs); // Bearbeitungsmethoden int finden(T& merkmal); void einfuegen(const T& merkmal); void loeschen(const T& merkmal); void bereinigeListe(void); int leererBaum(void) const; int baumGroesse(void) const; // baumspezifische Methoden void aktualisieren(const T& merkmal); baumKnoten<T> *holeWurzel(void) const; }; 17 Datenorganisation Die Schnittstellenfunktion void einfuegen(const T& merkmal); besitzt folgende Definition4: // Einfuegen "merkmal" in den Suchbaum template <class T> void binSBaum<T>::einfuegen(const T& merkmal) { // b ist der aktuelle Knoten beim Durchlaufen des Baums baumKnoten<T> *b = wurzel, *eltern = NULL, *neuerKnoten; // Terminiere beim leeren Teilbaum while(b != NULL) { // Aktualisiere den zeiger "eltern", // dann verzweige nach links oder rechts eltern = b; if (merkmal < b->daten) b = b->links; else b = b->rechts; } // Erzeuge den neuen Blattknoten neuerKnoten = holeBaumKnoten(merkmal,NULL,NULL); // Falls "eltern" auf NULL zeigt, einfuegen eines Wurzelknoten if (eltern == NULL) wurzel = neuerKnoten; // Falls merkmal < eltern->daten, einfuegen als linker Nachfolger else if (merkmal < eltern->daten) eltern->links = neuerKnoten; else // Falls merkmal >= eltern->daten, einfuegen als rechter Nachf. eltern->rechts = neuerKnoten; // Zuweisen "aktuell": "aktuell" ist die Adresse des neuen Knoten aktuell = neuerKnoten; groesse++; } b) Eine generische Klasse für den binären Suchbaum in Java5 Der binäre Suchbaum setzt voraus, dass alle Datenelemente in eine Ordnungsbeziehung6 gebracht werden können. Eine generische Klasse für einen binären Suchbaum erfordert daher ein Interface, das Ordnungsbeziehungen zwischen Daten eines Datenbestands festlegt. Diese Eigenschaft besitzt das Interface Comparable: public interface Comparable { int compareTo(Comparable rs) } Das Interface7 zeigt, dass zwei Datenelemente über die Methode „compareTo“ verglichen werden können. Über die Defintion eines Interface wird auch der zugehörige Referenztyp erzeugt, der wie andere Datentypen eingesetzt werden kann. // Freier binaerer Intervallbaum // Generische Klasse fuer einen unausgeglichenen binaeren Suchbaum 4 vgl. bsbaum.h vgl. pr42110 6 vgl. Kapitel 1, 1.2.2.2 7 Java-Interfaces werden in der Regel als eine Form abstrakter Klassen beschrieben, durch die einzelne Klassen der Hierarchie zusätzliche Funktionalität erhalten können. Sollen die Klassen diese Funktionalität durch Vererbung erhalten, müssten sie in einer gemeinsamen Superklasse angesiedelt werden. Weil ein Interface keine Implementierung enthält, kann auch keine Instanz davon erzeugt werden. Eine Klasse, die nicht alle Methoden eines Interface implementiert, wird zu einer abstrakten Klasse. 5 18 Datenorganisation // // Konstruktor: Initialisierung der Wurzel mit dem Wert null // // ************ oeffentlich zugaengliche Methoden******************** // void insert( x ) --> Fuege x ein // void remove( x ) --> Entferne x // Comparable find( x ) --> Gib das Element zurueck, das zu x passt // Comparable findMin( ) --> Rueckgabe des kleinsten Elements // Comparable findMax( ) --> Rueckgabe des groessten Elements // boolean isEmpty( ) --> Return true if empty; else false // void makeEmpty( ) --> Entferne alles // void printTree( ) --> Ausgabe der Binaerbaum-Elemente in sortierter // Folge // void ausgBinaerBaum() --> Ausgabe der Binaerbaum-Elemente um 90 Grad vers. /* * Implementiert einen unausgeglichenen binaeren Suchbaum. * Das Einordnen in den Suchbaum basiert auf der Methode compareTo */ public class BinaererSuchbaum { // Private Datenelemente /* Die Wurzel des Baums */ private BinaerBaumknoten root = null; /* Container mit daten zum Erstellen eines Baums */ Vector einVector; // Oeffentlich zugaengliche Methoden /* * Konstruktoren */ public BinaererSuchbaum() { root = null; } public BinaererSuchbaum(Vector einVector) { this.einVector = einVector; for (int i = 0; i < einVector.size();i++) { insert((Comparable) einVector.elementAt(i)); } } /* * Einfuegen eines Elements in den binaeren Suchbaum * Duplikate werden ignoriert */ public BinaerBaumknoten getRoot() { return root; } public void insert( Comparable x ) { root = insert(x, root); } /* * Entfernen aus dem Baum. Falls x nicht da ist, geschieht nichts */ public void remove( Comparable x ) { root = remove(x, root); } /* * finde das kleinste Element im Baum */ 19 Datenorganisation public Comparable findMin() { return elementAt(findMin(root)); } /* * finde das groesste Element im Baum */ public Comparable findMax() { return elementAt(findMax(root)); } /* * finde ein Datenelement im Baum */ public Comparable find(Comparable x) { return elementAt(find(x, root)); } /* * Mache den Baum leer */ public void makeEmpty() { root = null; } /* * Test, ob der Baum leer ist */ public boolean isEmpty() { return root == null; } /* * Ausgabe der Datenelemente in sortierter Reihenfolge */ public void printTree() { if( isEmpty( ) ) System.out.println( "Baum ist leer" ); else printTree( root ); } /* * Ausgabe der Elemente des binaeren Baums um 90 Grad versetzt */ public void ausgBinaerBaum() { if( isEmpty() ) System.out.println( "Leerer baum" ); else ausgBinaerBaum( root,0 ); } // Private Methoden /* * Methode fuer den Zugriff auf ein Datenelement */ private Comparable elementAt( BinaerBaumknoten b ) { return b == null ? null : b.daten; } /* * Interne Methode fuer das Einfuegen in einen Teilbaum */ private BinaerBaumknoten insert(Comparable x, BinaerBaumknoten b) { /* 1*/ if( b == null ) 20 Datenorganisation /* 2*/ b = new BinaerBaumknoten( x, null, null ); /* 3*/ else if( x.compareTo( b.daten ) < 0 ) /* 4*/ b.links = insert( x, b.links ); /* 5*/ else if( x.compareTo( b.daten ) > 0 ) /* 6*/ b.rechts = insert( x, b.rechts ); /* 7*/ else /* 8*/ ; // Duplikat; tue nichts /* 9*/ return b; } /* * Interne Methode fuer das Entfernen eines Knoten in einem Teilbaum */ private BinaerBaumknoten remove(Comparable x, BinaerBaumknoten b) { if( b == null ) return b; // nichts gefunden; tue nichts if( x.compareTo(b.daten) < 0 ) b.links = remove(x, b.links ); else if( x.compareTo(b.daten) > 0 ) b.rechts = remove( x, b.rechts ); else if( b.links != null && b.rechts != null ) // Zwei Kinder { b.daten = findMin(b.rechts).daten; b.rechts = remove(b.daten, b.rechts); } else b = ( b.links != null ) ? b.links : b.rechts; return b; } /* * Interne Methode zum Bestimmen des kleinsten Datenelements im Teilbaum */ private BinaerBaumknoten findMin(BinaerBaumknoten b) { if (b == null) return null; else if( b.links == null) return b; return findMin(b.links ); } /* * Interne Methode zum Bestimmen des groessten Datenelements im Teilbaum */ private BinaerBaumknoten findMax( BinaerBaumknoten b) { if( b != null ) while( b.rechts != null ) b = b.rechts; return b; } /* * Interne Methode zum Bestimmen eines Datenelements im Teilbaum. */ private BinaerBaumknoten find(Comparable x, BinaerBaumknoten b) { if(b == null) return null; if( x.compareTo(b.daten ) < 0) return find(x, b.links); else if( x.compareTo(b.daten) > 0) return find(x, b.rechts); else return b; // Gefunden! } /* * Internae Methode zur Ausgabe eines Teilbaums in sortierter Reihenfolge 21 Datenorganisation */ private void printTree(BinaerBaumknoten b) { if(b != null) { printTree(b.links); System.out.print(b.daten); System.out.print(' '); printTree( b.rechts ); } } /* * Ausgabe des Binaerbaums um 90 Grad versetzt */ private void ausgBinaerBaum(BinaerBaumknoten b, int stufe) { if (b != null) { ausgBinaerBaum(b.links, stufe + 1); for (int i = 0; i < stufe; i++) { System.out.print(' '); } System.out.println(b.daten); ausgBinaerBaum(b.rechts, stufe + 1); } } 3. Löschen eines Knoten Es soll ein Knoten mit einem bestimmten Schlüsselwert entfernt werden. Fälle A) Der zu löschende Knoten ist ein Blatt Bsp.: vorher nachher Abb.: Das Entfernen kann leicht durchgeführt werden B) Der zu löschende Knoten hat genau einen Sohn 22 Datenorganisation nachher vorher Abb.: C) Der zu löschende Knoten hat zwei Söhne nachher vorher Abb.: Der Knoten k wird durch den linken Sohn ersetzt. Der rechte Sohn von k wird rechter Sohn der rechtesten Ecke des linken Teilbaums. Der resultierende Teilbaum T' ist ein Suchbaum, häufig allerdings mit erheblich vergrößerter Höhe. Aufgaben: 1. Gegeben ist ein binärer Baum folgender Gestalt: k k1 k2 23 Datenorganisation k3 Die Wurzel wird gelöscht. Welche Gestalt nimmt der Baum dann an: 24 Datenorganisation k1 k3 k2 Abb.: Es ergibt sich eine Höhendifferenz ∂H , die durch folgende Beziehung eingegrenzt ist: −1 ≤ ∂H ≤ H (TL ) H (TL ) ist die Höhe des linken Teilbaums. 2. Gegeben ist die folgende Gestalt eines binären Baums 12 7 15 5 2 13 6 14 Welche Gestalt nimmt dieser Baum nach dem Entfernen der Schlüssel mit den unter a) bis f) angegebenen Werten an? a) 2 b) 6 25 Datenorganisation 12 7 15 5 13 14 c) 13 12 7 15 5 14 d) 15 12 7 14 5 e) 5 12 7 14 f) 12 7 14 26 Datenorganisation Schlüsseltransfer Der angegebene Algorithmus zum Löschen von Knoten kann zu einer beträchtlichen Vergrößerung der Baumhöhe führen. Das bedeutet auch eine beträchtliche Steigerung des mittleren Suchaufwands. Man ersetzt häufig die angegebene Verfahrensweise durch ein anderes Verfahren, das unter dem Namen Schlüsseltransfer bekannt ist. Der zu löschende Schlüssel (Knoten) wird ersetzt durch den kleinsten Schlüssel des rechten oder den größten Schlüssel des linken Teilbaums. Dieser ist dann nach Fall A) bzw. B) aus dem Baum herauszunehmen. Bsp.: Abb.: Test der Verfahrensweise "Schluesseltransfer": 1) Der zu löschende Baumknoten besteht nur aus einem Wurzelknoten, z.B.: Schlüssel LINKS 12 RECHTS Ergebnis: Der Wurzelknoten wird gelöscht. 2) Vorgegeben ist 27 Datenorganisation Schlüssel LINKS 12 RECHTS 7 5 8 Abb.: Der Wurzelknoten wird gelöscht. Ergebnis: 7 5 8 Abb.: 3) Vorgegeben ist 28 Datenorganisation Schlüssel LINKS 12 RECHTS 7 5 15 8 13 14 Abb.: Der Wurzelknoten wird gelöscht. Ergebnis: Schlüssel LINKS 13 RECHTS 7 5 15 8 14 Abb.: 29 Datenorganisation a) Implementierung der Verfahrenweise Schlüsseltransfer Baumknoten in einem binären Suchbaum in C++ zum Löschen von // Falls "merkmal" im Baum vorkommt, dann loesche es template <class T> void binSBaum<T>::loeschen(const T& merkmal) { // LKnoZgr: Zeiger auf Knoten L, der geloescht werden soll // EKnoZgr: Zeiger auf die "eltern" E des Knoten L // ErsKnoZgr: Zeiger auf den rechten Knoten R, der L ersetzt baumKnoten<T> *LKnoZgr, *EKnoZgr, *ErsKnoZgr; // Suche nach einem Knoten, der einen Knoten enthaelt mit dem // Datenwert von "merkmal". Bestimme die aktuelle Adresse diese Knotens // und die seiner "eltern" if ((LKnoZgr = findeKnoten (merkmal, EKnoZgr)) == NULL) return; // Falls LKnoZgr einen NULL-Zeiger hat, ist der Ersatzknoten // auf der anderen Seite des Zweigs if (LKnoZgr->rechts == NULL) ErsKnoZgr = LKnoZgr->links; else if (LKnoZgr->links == NULL) ErsKnoZgr = LKnoZgr->rechts; // Beide Zeiger von LKnoZgr sind nicht NULL else { // Finde und kette den Ersatzknoten fuer LKnoZgr aus. // Beginne am linkten Zweig des Knoten LKnoZgr, // bestimme den Knoten, dessen Datenwert am groessten // im linken Zweig von LKnoZgr ist. Kette diesen Knoten aus. // EvonErsKnoZgr: Zeiger auf die "eltern" des zu ersetzenden Knoten baumKnoten<T> *EvonErsKnoZgr = LKnoZgr; // erstes moegliches Ersatzstueck: linker Nachfolger von L ErsKnoZgr = LKnoZgr->links; // steige den rechten Teilbaum des linken Nachfolgers von LKnoZgr hinab, // sichere den Satz des aktuellen Knoten und den seiner "Eltern" // Beim Halt, wurde der zu ersetzende Knoten gefunden while(ErsKnoZgr->rechts != NULL) { EvonErsKnoZgr = ErsKnoZgr; ErsKnoZgr = ErsKnoZgr->rechts; } if (EvonErsKnoZgr == LKnoZgr) // Der linke Nachfolger des zu loeschenden Knoten ist das // Ersatzstueck // Zuweisung des rechten Teilbaums ErsKnoZgr->rechts = LKnoZgr->rechts; else { // es wurde sich um mindestens einen Knoten nach unten bewegt // der zu ersetzende Knoten wird durch Zuweisung seines // linken Nachfolgers zu "Eltern" geloescht EvonErsKnoZgr->rechts = ErsKnoZgr->links; // plaziere den Ersatzknoten an die Stelle von LKnoZgr ErsKnoZgr->links = LKnoZgr->links; ErsKnoZgr->rechts = LKnoZgr->rechts; } } // Vervollstaendige die Verkettung mit den "Eltern"-Knoten // Loesche den Wurzelknoten, bestimme eine neue Wurzel if (EKnoZgr == NULL) wurzel = ErsKnoZgr; // Zuweisen Ers zum korrekten Zweig von E else if (LKnoZgr->daten < EKnoZgr->daten) EKnoZgr->links = ErsKnoZgr; Else EKnoZgr->rechts = ErsKnoZgr; // Loesche den Knoten aus dem Speicher und erniedrige "groesse" freigabeKnoten(LKnoZgr); groesse--; } 30 Datenorganisation b) Implementierung der Verfahrenweise Schlüsseltransfer Baumknoten in einem binären Suchbaum in Java zum Löschen von /* * Interne Methode fuer das Entfernen eines Knoten in einem Teilbaum */ private BinaerBaumknoten remove(Comparable x, BinaerBaumknoten b) { if( b == null ) return b; // nichts gefunden; tue nichts if( x.compareTo(b.daten) < 0 ) b.links = remove(x, b.links ); else if( x.compareTo(b.daten) > 0 ) b.rechts = remove( x, b.rechts ); else if( b.links != null && b.rechts != null ) // Zwei Kinder { b.daten = findMin(b.rechts).daten; b.rechts = remove(b.daten, b.rechts); } else b = ( b.links != null ) ? b.links : b.rechts; return b; } 4.2.3 Ordnungen und Durchlaufprinzipien Das Prinzip, wie ein geordneter Baum durchlaufen wird, legt eine Ordnung auf der Menge der Knoten fest. Es gibt 3 Möglichkeiten (Prinzipien), die Knoten eines binären Baums zu durchlaufen: 1. Inordnungen LWR-Ordnung (1) Durchlaufen (Aufsuchen) des linken Teilbaums in INORDER (2) Aufsuchen der BAUMWURZEL (3) Durchlaufen (Aufsuchen) des rechten Teilbaums in INORDER RWL-Ordnung (1) Durchlaufen (Aufsuchen) des rechten Teilbaums in INORDER (2) Aufsuchen der BAUMWURZEL (3) Durchlaufen (Aufsuchen) des Teilbaums in INORDER Der LWR-Ordnung und die RWL-Ordnung sind zueinander invers. Die LWR Ordnung heißt auch symmetrische Ordnung. 2. Präordnungen WLR-Ordnung (1) Aufsuchen der BAUMWURZEL (2) Durchlaufen (Aufsuchen) des linken Teilbaums in PREORDER (3) Durchlaufen (Aufsuchen) des rechten Teilbaums in PREORDER 31 Datenorganisation WRL-Ordnung (1) Aufsuchen der BAUMWURZEL (2) Durchlaufen (Aufsuchen) des rechten Teilbaums in PREORDER (3) Durchlaufen (Aufsuchen) des linken Teilbaums in PREORDER Es wird hier grundsätzlich die Wurzel vor den (beiden) Teilbäumen durchlaufen. 3. Postordnungen LRW-Ordnung (1) Durchlaufen (Aufsuchen) des linken Teilbaums in POSTORDER (2) Durchlaufen (Aufsuchen) des rechten Teilbaums in POSTORDER (3) Aufsuchen der BAUMWURZEL Zunächst werden die beiden Teilbäume und dann die Wurzel durchlaufen. RLW-Ordnung (1) Durchlauden (Aufsuchen) des rechten Teilbaums in POSTORDER (2) Durchlaufen (Aufsuchen) des linken Teilbaums in POSTORDER (3) Aufsuchen der BAUMWURZEL Zunächst werden die beiden Teilbäume und dann die Wurzel durchlaufen. a) Funktionsschablonen für das Durchlaufen binärer Bäume in C++ // Funktionsschablonen fuer Baumdurchlaeufe template <class T> void inorder(baumKnoten<T>* b, void aufsuchen(T& merkmal)) { if (b != NULL) { inorder(b->holeLinks(),aufsuchen); aufsuchen(b->daten); inorder(b->holeRechts(),aufsuchen); } } template <class T> void postorder(baumKnoten<T>* b, void aufsuchen(T& merkmal)) { if (b != NULL) { postorder(b->holeLinks(),aufsuchen); // linker Abstieg postorder(b->holeRechts(),aufsuchen); // rechter Abstieg aufsuchen(b->daten); } } b) Rekursive Ausgabefunktion in Java8 public static void print (StringBinaerBaumknoten baum) // Rekursive Druckfunktion { 8 vgl. pr42100 32 Datenorganisation if (baum == null) return; print(baum.getLinks()); System.out.println(baum.getString()); print(baum.getRechts()); } Aufgaben: Gegeben sind eine Reihe binärer Bäume. Welche Folgen entstehen beim Durchlaufen der Knoten nach den Prinzipien "Inorder (LWR)", "Praeorder WLR" und "Postorder (LRW)". 1. A B C E D F I G H J K L "Praeorder": A B C E I F J D G H K L "Inorder": EICFJBGDKHLA "Postorder": I E J F C G K L H D B A 2. + * A + B * C E D "Praeorder": + * A B + * C D E "Inorder": A*B+C*D+E "Postorder": A B * C D * E + + Diese Aufgabe zeigt einen Strukturbaum (Darstellung der hierarchischen Struktur eines arithmetischen Ausdrucks). Diese Baumdarstellung ist besonders günstig für die Übersetzung eines Ausdrucks in Maschinensprache. Aus der vorliegenden Struktur lassen sich leicht die unterschiedlichen Schreibweisen eines arithmetischen Ausdrucks herleiten. So liefert das Durchwandern des Baums in "Postorder" die Postfixnotation, in "Praeorder" die Praefixnotation". 3. + 33 Datenorganisation A * B C "Praeorder": + A * B C "Inorder": A+B*C "Postorder": A B C * + 4. * + A C B "Praeorder": * + A B C "Inorder": A+B*C "Postorder": A B + C * 34 Datenorganisation Anwendungen der Durchlaufprinzipien Mit Hilfe der angegebenen Ordnungen bzw. Durchlaufprinzipien lassen sich weitere Operationen auf geordneten Wurzelbäumen bestimmen: a) C++-Anwendungen 1. Bestimmen der Anzahl Blätter im Baum // Anzahl Blätter template <class T> void anzBlaetter(baumKnoten<T>* b, int& zaehler) { // benutze den Postorder-Durchlauf if (b != NULL) { anzBlaetter(b->holeLinks(), zaehler); anzBlaetter(b->holeRechts(), zaehler); // Pruefe, ob der erreichte Knoten ein Blatt ist if (b->holeLinks() == NULL && b->holeRechts() == NULL) zaehler++; } } 2. Ermitteln der Höhe des Baums // Hoehe des Baums template <class T> int hoehe(baumKnoten<T>* b) { int hoeheLinks, hoeheRechts, hoeheWert; if (b == NULL) hoeheWert = -1; else { hoeheLinks = hoehe(b->holeLinks()); hoeheRechts = hoehe(b->holeRechts()); hoeheWert = 1 + (hoeheLinks > hoeheRechts ? hoeheLinks : hoeheRechts); } return hoeheWert; } 3. Kopieren des Baums // Kopieren eines Baums template <class T> baumKnoten<T>* kopiereBaum(baumKnoten<T>* b) { baumKnoten<T> *neuerLzgr, *neuerRzgr, *neuerKnoten; // Rekursionsendebedingung if (b == NULL) return NULL; if (b->holeLinks() != NULL) neuerLzgr = kopiereBaum(b->holeLinks()); else neuerLzgr = NULL; if (b->holeRechts() != NULL) neuerRzgr = kopiereBaum(b->holeRechts()); else neuerRzgr = NULL; // Der neue Baum wird von unten her aufgebaut, // zuerst werden die Nachfolger bearbeitet und // dann erst der Vaterknoten 35 Datenorganisation neuerKnoten = erzeugebaumKnoten(b->daten, neuerLzgr, neuerRzgr); // Rueckgabe des Zeigers auf den zuletzt erzeugten Baumknoten return neuerKnoten; } 4. Löschen des Baums // Loeschen des Baums template <class T> void loescheBaum(baumKnoten<T>* b) { if (b != NULL) { loescheBaum(b->holeLinks()); loescheBaum(b->holeRechts()); gibKnotenFrei(b); } } b) Java-Anwendungen Grafische Darstellung eines binaeren Suchbaums9 private void zeichneBaum(BinaerBaumknoten b, int x, int y, int m, int s) { Graphics g = meinCanvas.getGraphics(); if (b != null) { if (b.links != null) { g.drawLine(x,y,x - m / 2,y + s); zeichneBaum(b.links,x - m / 2,y + s,m / 2,s); } if (b.rechts != null) { g.drawLine(x,y,x + m / 2,y+s); zeichneBaum(b.rechts,x + m / 2,y + s,m / 2,s); } } } } 9 vgl. pr42110 36 Datenorganisation 4.3 Balancierte Bäume Hier geht es darum, entartete Bäume (schiefe Bäume, Äste werden zu linearen Listen, etc.) zu vermeiden. Statische Optimierung heißt: Der ganze Baum wird neu (bzw. wieder neu) aufgebaut. Bei der dynamischen Optimierung wird der Baum während des Betriebs (bei jedem Ein- und Ausfügen) optimiert. Perfekt ausgeglichener, binärer Suchbaum Ein binärer Suchbaum sollte immer ausgeglichen sein. Der folgende Baum 1 2 3 4 5 ist zu einer linearen Liste degeneriert und läßt sich daher auch nicht viel schneller als eine lineare Liste durchsuchen. Ein derartiger binärer Suchbaum entsteht zwangsläufig, wenn die bei der Eingabe angegebene Schlüsselfolge in aufsteigend sortierter Reihenfolge vorliegt. Der vorliegende binäre Suchbaum ist selbstverständlich nicht ausgeglichen. Es gibt allerdings auch Unterschiede bei der Beurteilung der Ausgeglichenheit, z.B.: Die vorliegenden Bäume sind beide ausgeglichen. Der linke Baum ist perfekt ausbalanciert. Jeder Binärbaum ist perfekt ausbalanciert, falls jeder Knoten über einen linken und rechten Teilbaum verfügt, dessen Knotenzahl sich höchstens um den Wert 1 unterscheidet. Der rechte Teilbaum ist ein in der Höhe ausgeglichener (AVL10-)Baum. Die Höhe der Knoten zusammengehöriger linker und rechter Teilbäume unterscheiden sich höchstens um den Wert 1. Jeder perfekt ausgeglichene Baum ist gleichzeitig auch ein in der Höhe ausgeglichener Binärbaum. Der umgekehrte Fall trifft allerdings nicht zu. 10 nach den Afangsbuchstaben der Namen seiner Entdecker: Adelson, Velskii u. Landes 37 Datenorganisation Es gibt einen einfachen Algorithmus zum Erstellen eines pefekt ausgeglichenen Binärbaums, falls (1) die einzulesenden Schlüsselwerte sortiert in aufsteigender Reihenfolge angegeben werden (2) bekannt ist, wieviel Objekte (Schlüssel) werden müssen. import java.io.*; class PBBknoten { // Instanzvariable protected PBBknoten links; // linker Teilbaum protected PBBknoten rechts; // rechter Teilbaum public int daten; // Dateninhalt der Knoten // Konstruktoren public PBBknoten() { this(0,null,null); } public PBBknoten(int datenElement) { this(datenElement, null, null ); } public PBBknoten(int datenElement, PBBknoten l, PBBknoten r) { daten = datenElement; links = l; rechts = r; } public PBBknoten getLinks() { return links; } public PBBknoten getRechts() { return rechts; } } public class PBB { static BufferedReader ein = new BufferedReader(new InputStreamReader( System.in)); // Instanzvariable PBBknoten wurzel; // Konstruktor public PBB(int n) throws IOException { if (n == 0) wurzel = null; else { int nLinks = (n - 1) / 2; int nRechts = n - nLinks - 1; wurzel = new PBBknoten(); wurzel.links = new PBB(nLinks).wurzel; wurzel.daten = Integer.parseInt(ein.readLine()); wurzel.rechts = new PBB(nRechts).wurzel; } } public void ausgPBB() { 38 Datenorganisation ausg(wurzel,0); } private void ausg(PBBknoten b, int nSpace) { if (b != null) { ausg(b.rechts,nSpace += 6); for (int i = 0; i < nSpace; i++) System.out.print(" "); System.out.println(b.daten); ausg(b.links, nSpace); } } // Test public static void main(String args[]) throws IOException { int n; System.out.print("Gib eine ganze Zahl n an, "); System.out.print("gefolgt von n ganzen Zahlen in "); System.out.println("aufsteigender Folge"); n = Integer.parseInt(ein.readLine()); PBB b = new PBB(n); System.out.print( "Hier ist das Resultat: ein perfekt balancierter Baum, "); System.out.println("die Darstellung erfogt um 90 Grad versetzt"); b.ausgPBB(); } } Schreibtischtest: Wird mit n = 10 aufgerufen, dann wird anschließend die Anzahl der Knoten berechnet, die sowohl in den linken als auch in den rechten Teilbaum eingefügt werden. Da der Wurzelknoten keinem Teilbaum zugeordnet werden kann, ergeben sich für die beiden Teilbäume (10 – 1) Knoten. Das ergibt nLinks = 4, nRechts = 5. Anschließend wird der Wurzelknoten erzeugt. Es folgt der rekursive Aufruf wurzel.links = new PBB(nLinks).wurzel; mit nLinks = 4. Die Folge davon ist: Einlesen von 4 Zahlen und Ablage dieser Zahlen im linken Teilbaum. Die danach folgende Zahl wird im Wurzelknoten abgelegt. Der rekursive Aufruf wurzel.rechts = new PBB(nRechts).wurzel; mit nRechts = 5 verarbeitet die restlichen 5 Zahlen und erstellt damit den rechten Teilbaum. Durch jedem rekursiven Aufruf wird ein Baum mit zwei ungefähr gleich großen Teilbäumen erzeugt. Da die im Wurzelknoten enthaltene Zahl direkt nach dem erstellen des linken Teilbaum gelesen wird, ergibt sich bei aufsteigender Reihenfolge der Eingabedaten ein binärer Suchbaum, der außerdem noch perfekt balanciert ist. 39 Datenorganisation 4.3.1 Statisch optimierte Bäume Der Algorithmus zum Erstellen eines perfekt ausgeglichenen Baums kann zur statischen Optimierung binärer Suchbäume verwendet werden. Das Erstellen des binären Suchbaums erfolgt dabei nach der bekannten Verfahrensweise. Wird ein solcher Baum in Inorder-Folge durchlaufen, so werden die Informationen in den Baumknoten aufsteigend sortiert. Diese sortierte Folge ist Eingangsgröße für die statische Optimierung. Es wird mit der sortiert vorliegende Folge der Schlüssel ein perfekt ausgeglichener Baum erstellt. Bsp.: Ein Java-Applet zur Demonstration der statischen Optimierung.11 Zufallszahlen werden generiert und in einen freien binären Intervallbaum aufgenommen, der im oberen Bereich des Fensters gezeigt werden. Über Sortieren erfolgt ein Inorder-Durchlauf des binären Suchbaums, über „Perfekter BinaerBaum“ Erzeugen und Darstellen des perfekt ausgeglichen binären Suchbaums. Abb.: 11 vgl. pr43205, ZPBBApplet.java und ZPBBApplet.html 40 Datenorganisation 4.3.2 AVL-Baum Der wohl bekannteste dynamisch ausgeglichene Binärbaum ist der AVL-Baum, genannt nach dem Anfangsbuchstaben seiner Entdecker (Adelson, Velskii und Landis). Ein Baum hat die AVL-Eigenschaft, wenn in jedem Knoten sich die Höhen der beiden Unterbäume höchstens um 1 (|HR - HL| <= 1) unterscheiden. Die Last ("Balance") muß in einem Knoten mitgespeichert sein. Es genügt aber als Maß für die Unsymmetrie die Höhendifferenz ∂H festzuhalten, die nur die Werte -1 (linkslastig), 0 (gleichlastig) und +1 (rechtslastig) annehmen kann. 1. Einfügen Beim Einfügen eines Knoten können sich die Lastverhältnisse nur auf dem Wege, den der Suchvorgang in Anspruch nimmt, ändern. Der tatsächliche Ort der Änderung ist aber zunächst unbekannt. Der Knoten ist deshalb einzufügen und auf notwendige Berichtigungen zu überprüfen. Bsp.: Gegeben ist der folgende binäre Baum 8 4 2 10 6 Abb.: 41 Datenorganisation 1) In diesen Baum sind die Knoten mit den Schlüsseln 9 und 11 einzufügen. Die Gestalt des Baums ist danach: 8 4 2 10 6 9 11 Abb.: Die Schlüsel 9 und 11 können ohne zusätzliches Ausgleichen eingefügt werden. 2) In den gegebenen Binärbaum sind die Knoten mit den Schlüsseln 1, 3, 5 und 7 einzufügen. Wie ist die daraus resultierende Gestalt des Baums beschaffen? 8 4 2 10 6 1 Abb.: Schon nach dem Einfügen des Schlüsselwerts „1“ ist anschließendes Ausgleichen unbedingt erforderlich. 3) Wie könnte das Ausgleichen vollzogen werden? Eine Lösungsmöglichkeit ist hier bspw. eine einfache bzw. eine doppelte Rotation. Einfache Rotation 8 2 1 10 4 6 42 Datenorganisation Diese einfache Rotation führt zu keiner Lösung. Doppelte Rotation 4 2 1 8 3 6 10 Nach der doppelten Rotation kann der Schlüssel mit dem Wet „3“ problemlos eingefügt werden. b) Beschreibe den Ausgleichsvorgang, nachdem die Schlüssel 5 und 7 eingefügt wurden! 4 2 1 8 3 6 10 5 7 Abb. Das Einfügen der Schlüssel mit den Werten „5“ und „7“ verletzt die AVL-Eigenschaft nicht Nachdem ein Knoten eingefügt ist, ist der Pfad, den der Suchvorgang für das Einfügen durchlaufen hat, aufwärts auf notwendige Berichtigungen zu überprüfen. Bei dieser Prüfung wird die Höhendifferenz des linken und rechten Teilbaums bestimmt. Es können generell folgende Fälle eintreten: (1) ∂H = +1 bzw. -1 Eine Verlängerung des Baums auf der Unterlastseite gleicht die Last aus, die Verlängerung wirkt sich nicht weiter nach oben aus. Die Prüfung kann abgebrochen werden. (2) ∂H = 0 Das bedeutet: Verlängerung eines Teilbaums Hier ist der Knoten dann ungleichlastig ( ∂H = +1 bzw. -1), die AVL-Eigenschaft bleibt jedoch insgesamt erhalten. Der Baum wurde länger. (3) ∂H = +1 bzw. -1 Das bedeutet: Verlängerung des Baums auf der Überlastseite. 43 Datenorganisation Die AVL-Eigenschaft ist verletzt, wenn ∂H = +2 bzw. -2. Sie wird durch Rotationen berichtigt. Die Information über die Ausgeglichenheit steht im AVL-Baumknoten, z.B.: struct knoten { int num, // Schluessel bal; // Ausgleichsfaktor struct knoten *zLinks, *zRechts; }; Abb.: AVL-Baumknoten mit Ausgleichfaktor in C++ In der Regel gibt es folgende Faktoren für die "Ausgeglichenheit" je Knoten im AVLBaum: "-1": Höhe des linken Teilbaums ist um eine Einheit (ein Knoten) größer als die Höhe im rechten Teilbaum. "0": Die Höhen des linken und rechten Teilbaums sind gleich. "1": Die Höhe des linken Teilbaums ist um eine Einheit (ein Knoten) kleiner als die Höhe des rechten Teilbaums. Bsp.: Die folgende Darstellung zeigt den Binärbaum unmittelbar nach dem Einfügen eines Baumknoten. Daher kann hier der Faktor für Ausgeglichenheit -2 bis 2 betragen. 12 +1 7 17 +1 +2 5 9 14 0 -1 0 24 +2 8 25 0 +1 30 0 Nach dem Algorithmus für das Einfügen ergibtsich folgender AVL-Baum: 12 0 7 17 +1 5 +1 9 14 25 44 Datenorganisation -1 0 8 24 0 0 30 0 45 Datenorganisation Es gibt 4 Möglichkeiten die Ausgeglichenheit, falls sie durch Einfügen eines Baumknoten gestört wurde, wieder herzustellen. A A A A B a a B B a B b c c c b b c b 1a 1b 2a 2b Abb.: Die vier Ausgangssituationen bei der Wiederherstellung der AVL-Eigenschaft Von den 4 Fällen sind jeweils die Fälle 1a, 1b und die Fälle 2a, 2b zueinander symmetrisch. Für den Fall 1a kann durch einfache Rotation eine Ausgeglichenheit erreicht werden. B 0 b A 0 c a Im Fall 1b muß die Rotation nach links erfolgen. 46 a Datenorganisation Für die Behandlung von Fall 2a der Abb. 1 wird der Teilbaum c aufgeschlüsselt in dessen Teilbäume c1 und c2: A -2 B a 2 b C -1 c1 c2 Abb.: Durch zwei einfache Rotationen kann der Baum ausgeglichen werden: 1. Rotation 2. Rotation A C -2 0 C a B A -2 +1 +1 B c2 b c1 c2 a +1 b c1 Abb.: a) Implementierung in C++ Mit der unter 1. festgestellten Verfahrensweise soll eine Klasse AVLBaum bestimmt werden, die Knoten der zuvor angegebenen Struktur so in einen Binärbaum einfügt, daß die AVL-Eigenschaft gewährleistet bleibt. // avl: Demonstrationsprogramm fuer AVL-Baeume // Einfuegen und Loeschen von Knoten 47 Datenorganisation #include <iostream.h> #include <iomanip.h> #include <ctype.h> struct knoten { int num, bal; struct knoten *zLinks, *zRechts; }; class AVLbaum { private: knoten *wurzel; void LinksRotation(knoten* &z); void RechtsRotation(knoten* &z); int einf(knoten* &z, int x); void aus(const knoten *z, int nLeerZeichen) const; public: AVLbaum():wurzel(NULL){} void einfuegen(int x){einf(wurzel, x);} void ausgabe()const{aus(wurzel, 0);} }; Methoden. void AVLbaum::LinksRotation(knoten* &z) { knoten *hz = z; z = z->zRechts; hz->zRechts = z->zLinks; z->zLinks = hz; hz->bal--; if (z->bal > 0) hz->bal -= z->bal; z->bal--; if (hz->bal < 0) z->bal += hz->bal; } void AVLbaum::RechtsRotation(knoten* &z) { knoten *hz = z; z = z->zLinks; hz->zLinks = z->zRechts; z->zRechts = hz; hz->bal++; if (z->bal < 0) hz->bal -= z->bal; z->bal++; if (hz->bal > 0) z->bal += hz->bal; } int AVLbaum::einf(knoten* &z, int x) { // Rueckgabewert: Anstieg in der Hoehe // (0 or 1) nach Einfuegen von x in den // Teilbaum mit Wurzel z int deltaH = 0; if (z == NULL) { z = new knoten; z->num = x; z->bal = 0; z->zLinks = z->zRechts = NULL; deltaH = 1; // Die Hoehe des Baums waechst um 1 } else if (x > z->num) { if (einf(z->zRechts, x)) { z->bal++; // Die Hoehe des rechten Teilbaums waechst if (z->bal == 1) deltaH = 1; 48 Datenorganisation else if (z->bal == 2) { if (z->zRechts->bal == -1) RechtsRotation(z->zRechts); LinksRotation(z); } } } else if (x < z->num) { if (einf(z->zLinks, x)) { z->bal--; // Hoehe vom linken Teilbaum waechst if (z->bal == -1) deltaH = 1; else if (z->bal == -2) { if (z->zLinks->bal == 1) LinksRotation(z->zLinks); RechtsRotation(z); } } } return deltaH; } b) Die generische Klasse „AvlBaum“ in Java Grundlagen: Die AVL-Eigenschaft ist verletzt, wenn diese Höhendifferenz +2 bzw. – 2 ist. Der Knoten, der diesen Wert erhalten hat, ist der Knoten „alpha“, dessen Unausgeglichenheit auf einen der folgenden 4 Fälle zurückzuführen ist: 1. Einfügen in den linken Teilbaum, der vom linken Nachkommen des Knoten „alpha“ bestimmt ist. 2. Einfügen in den rechten Teilbaum, der vom linken Nachkommen des Knoten „alpha“ bestimmt ist. 3. Einfügen in den linken Teilbaum, der vom rechten Nachkommen des Knoten „alpha“ bestimmt ist. 4. Einfügen in den rechten Teilbaum, der vom rechten Nachkommen des Knoten „alpha“ bestimmt ist Fall 1 und Fall 4 bzw. Fall 2 und Fall 3 sind Spiegelbilder, zeigen also das gleiche Verhalten. Fall 1 kann durch einfache Rotation behandelt werden und ist leicht zu bestimmen, daß das Einfügen „außerhalb“ (links – links bzw. rechts – rechts im Fall 4 stattfindet. Fall 2 kann durch doppelte Rotation behandelt werden und ist ebenfalls leicht zu bestimmen, da das Einfügen „innerhalb“ (links –rechts bzw. rechts – links) erfolgt. Die einfache Rotation: Die folgende Darstellung beschreibt den Fall 1 vor und nach der Rotation: 49 Datenorganisation k2 k1 k1 k2 Z Y X Y Z X Abb.: Die folgende Darstellung beschreibt Fall 4 vor und nach der Rotation : k1 k2 k2 k1 X Y X Y Z Z Abb.: Doppelrotation: Die einfache Rotation führt in den Fällen 2 und 3 nicht zum Erfolg. Fall 2 muß durch eine Doppelrotation (links – rechts) behandelt werden. k3 k2 k1 k1 k3 D k2 B A A B C Abb.: 50 C D Datenorganisation Auch Fall 3 muß durch Doppelrotation behandelt werden k1 k2 k2 A k1 k3 k3 D B A B C D C Abb.: Implementierung: Zum Einfügen eines Knoten mit dem Datenwert „x“ in einen AVLBaum, wird „x“ rekursiv in den betoffenen Teilbaum eingesetzt. Falls die Höhe dieses Teilbaums sich nicht verändert, ist das Einfügen beendet. Liegt Unausgeglichenheit vor, dann ist einfache oder doppelte Rotation (abhängig von „x“ und den Daten des betroffenen Teilbaums) nötig. Avl-Baumknoten12 Er enthält für jeden Knoten eine Angabe zur Höhe(ndifferenz) seiner Teilbäume. // Baumknoten fuer AVL-Baeume class AvlKnoten { // Instanzvariable protected AvlKnoten links; // Linkes Kind protected AvlKnoten rechts; // Rechtes Kind protected int hoehe; // Hoehe public Comparable daten; // Das Datenelement // Konstruktoren public AvlKnoten(Comparable datenElement) { this(datenElement, null, null ); } public AvlKnoten( Comparable datenElement, AvlKnoten lb, AvlKnoten rb ) { daten = datenElement; links = lb; rechts = rb; hoehe = 0; } } 12 vgl. pr43210 51 Datenorganisation Der Avl-Baum13 Bei jedem Schritt ist festzustellen, ob die Höhe des Teilnaums, in dem ein Element eingefügt wurde, zugenommen hat. /* * Rueckgabe: Hoehe des Knotens, oder -1, falls null. */ private static int hoehe(AvlKnoten b) { return b == null ? -1 : b.hoehe; } Die Methode „insert“ führt das Einfügen eines Baumknoten in den Avl-Baum aus: /* * Interne Methode zum Einfuegen eines Baumknoten in einen Teilbaum. * x ist das einzufuegende Datenelement. * b ist der jeweilige Wurzelknoten. * Rueckgabe der neuen Wurzel des jeweiligen Teilbaums. */ private AvlKnoten insert(Comparable x, AvlKnoten b) { if( b == null ) b = new AvlKnoten(x, null, null); else if (x.compareTo( b.daten) < 0 ) { b.links = insert(x, b.links ); if (hoehe( b.links ) - hoehe( b.rechts ) == 2 ) if (x.compareTo( b.links.daten ) < 0 ) b = rotationMitLinksNachf(b); else b = doppelrotationMitLinksNachf(b); } else if (x.compareTo( b.daten ) > 0 ) { b.rechts = insert(x, b.rechts); if( hoehe(b.rechts) - hoehe(b.links) == 2) if( x.compareTo(b.rechts.daten) > 0 ) b = rotationMitRechtsNachf(b); else b = doppelrotationMitRechtsNachf( b ); } else ; // Duplikat; tue nichts b.hoehe = max( hoehe( b.links ), hoehe( b.rechts ) ) + 1; return b; } Rotationen /* * Rotation Binaerbaumknoten mit linkem Nachfolger. * Fuer AVL-Baeume ist dies eine einfache Rotation (Fall 1). * Aktualisiert Angaben zur Hoehe, dann Rueckgabe der neuen Wurzel. */ private static AvlKnoten rotationMitLinksNachf(AvlKnoten k2) { AvlKnoten k1 = k2.links; k2.links = k1.rechts; k1.rechts = k2; k2.hoehe = max( hoehe( k2.links ), hoehe( k2.rechts ) ) + 1; 13 vgl. pr43210 52 Datenorganisation k1.hoehe = max( hoehe( k1.links ), k2.hoehe ) + 1; return k1; } /* * Rotation Binaerbaumknoten mit rechtem Nachfolger. * Fuer AVL-Bäume ist dies eine einfache Rotation (Fall 4). * Aktualisiert Angaben zur Hoehe,, danach Rueckgabe der neuen Wurzel. */ private static AvlKnoten rotationMitRechtsNachf(AvlKnoten k1) { AvlKnoten k2 = k1.rechts; k1.rechts = k2.links; k2.links = k1; k1.hoehe = max( hoehe( k1.links ), hoehe( k1.rechts ) ) + 1; k2.hoehe = max( hoehe( k2.rechts ), k1.hoehe ) + 1; return k2; } /* * Doppelrotation der Binaerbaumknoten: : erster linker Nachfolgeknoten * mit seinem rechten Nachfolger; danach Knoten k3 mit neuem linken * Nachfolgerknoten. * Fuer AVL-Baeume ist dies eine doppelte Rotation (Fall 2) * Aktualisiert Angaben zur Hoehe,, danach Rueckgabe der neuen Wurzel. */ private static AvlKnoten doppelrotationMitLinksNachf(AvlKnoten k3) { k3.links = rotationMitRechtsNachf( k3.links ); return rotationMitLinksNachf( k3 ); } /* * Doppelrotation der Binaerbaumknoten: erster rechter Nachfolgeknoten * mit seinem linken Nachfolger;danach Knoten k1 mit neuem rechten * Nachfolgerknoten * Fuer AVL-Baeume ist dies eine doppelte Rotation (Fall 3) * Aktualisiert Angaben zur Hoehe,, danach Rueckgabe der neuen Wurzel. */ private static AvlKnoten doppelrotationMitRechtsNachf(AvlKnoten k1) { k1.rechts = rotationMitLinksNachf(k1.rechts); return rotationMitRechtsNachf(k1); } } 53 Datenorganisation 2. Löschen Man kann folgende Fälle unterscheiden: (1) ∂H = +1 bzw. -1 (Verkürzung des Teilbaums auf der Überlastseite) (2) ∂H = 0 (Verkürzung eines Unterbaums) Der Knoten ist jetzt ungleichlastig ( ∂H = +1 bzw. -1), bleibt jedoch im Rahmen der AVL-Eigenschaft. Der Baum hat seine Höhe nicht verändert, die Berichtigung kann abgebrochen werden. (3) ∂H = +1 bzw. -1 (Verkürzung eines Baums auf der Unterlastseite) Die AVL-Eigenschaft ist verletzt, falls ∂H = +2 bzw. -2. Sie wird durch eine Einfachbzw. Doppelrotation wieder hergestellt. Dadurch kann sich der Baum verkürzen, so daß Lastreduzierungen an den Vorgänger weiterzugeben sind. Es können aber auch Lastsituationen mit dem Lastfaktor 0 auftreten. Bsp.: Spezialfälle zum Lastausgleich in einem AVL-Baum k k' H+2 H+1 k' a k H+1 H+1 b c c H H+1 a b k k'' H+1 H+2 k' a k'' k H H a b d H H Abb.: Löschen kann in der Regel nicht mit einer einzigen Rotation abgeschlossen werden. Im schlimmsten Fall erfordern alle Knoten im Pfad der Löschstelle eine Rekonfiguration. Experimente zeigen allerdings, daß beim Einfügen je Operation mehr Rotationen durchzuführen sind als beim Löschen. Offenbar existieren beim Löschen durch den inneren Bereich des Baums mehr Knoten, die ohne weiteres eliminiert werden können. 54 Datenorganisation Aufgabe 1. Gegeben ist die Schlüsselfolge 7, 6, 8, 5, 9, 4. Ermittle, wie sich mit dieser Schlüsselfolge einen AVL-Baum aufbaut. Schlüssel 7 BALANCE 0 LINKS, RECHTS 5 8 0 -1 4 6 0 0 9 0 Abb.: 2. Aus dem nun vorliegenden AVL-Baum sind die Knoten mit den Schlüsselwerten 9 und 8 zu löschen. Gib an, welche Gestalt der AVL-Baum jeweils annimmt. Schlüssel 5 BALANCE 1 LINKS, RECHTS 7 4 0 -1 6 0 Abb.: 55 Datenorganisation 4.3.3 Splay-Bäume Zugrundeliegende Idee Nachdem auf einen Baumknoten zugegriffen wurde, wird dieser Knoten über eine Reihe von AVL-Rotationen zur Wurzel. Bis zu einem gewissen Grade führt das zur Ausbalancierung. Realisierung über einfache Rotationen Am einfachsten wäre die Ausführung einfacher Rotationen von Grund auf. Jeder Knoten rotiert auf dem Zugriffspfad14 mit den Elternknoten, z.B.: k5 k4 k3 F E k2 D k1 A B C 1. Einfache Rotation zwischen k1 und seinem Eltern-Knoten 14 Der Zugriffspfad ist gestrichelt 56 Datenorganisation k5 k4 F k3 E k1 D k2 C A B 2. Einfache Rotation zwischen k1 und k3 k5 k4 F k1 E k2 A k3 B C D 3. Zwei weitere Rotationen führen dazu, daß k1 die Wurzel erreicht 57 Datenorganisation k5 k1 F k2 A k4 B E k3 C D k1 k2 A k5 B k4 F E K3 C D Bei den Rotationen wird allerdings der Knoten k3 beinahe auf eine Tiefe heruntergeschoben wie zuvor der Knoten k1. Splaying Die „Splaying“-Strategie folgt der Idee mit den Rotationen, benutzt dazu aber eine sorgfältige Auswahl. Auch hier wird vom Grund des Zugriffspfads aus rotiert. Erfolgt der Zugriff auf den Knoten X, dann sind folgende Fälle zu unterscheiden: 1. X ist kein Wurzelknoten, der Elternknoten von X ist bereits die Wurzel Hier genügt eine einfache Rotation zwischen X und der Wurzel 2. X ist kein Wurzelknoten, besitzt aber Eltern- und Großelternknoten Hier sind zwei Fälle zu unterscheiden: 58 Datenorganisation a) G X P D P G à A A B C D X B C Abb.: “Zig-zag” Ausgeführt wird eine doppelte AVL-Rotation b) G P X D A P à B X G C C A B Abb.: Zig-zig Ausgeführt wird eine einfache Rotation (von links nach rechts) 59 D Datenorganisation Bsp.: Zig-zag- bzw. Zig-Zig-Rotationen Ausgangslage: Zugriff auf k1 k5 k4 F k3 E k2 D k1 A B C 1. Zig-zag k5 k4 F k1 E k2 A k3 B C D 2. Zig-zig k1 k2 A k4 B k3 C k5 D 60 E F Datenorganisation 4.3.4 Rot-Schwarz-Bäume Zum Ausschluß des ungünstigsten Falls bei binären Suchbäumen ist eine gewisse Flexibilität in den verwendeten Datenstrukturen nötig. Das kann bspw. durch Aufnahme von mehr als einem Schlüssel in Baumknoten erreicht werden. So soll es 3-Knoten bzw. 4-Knoten geben, die 2 bzw. 3 Schlüssel enthalten können: - Ein 3-Knoten besitzt 3 von ihm ausgehende Verkettungen -- eine für alle Datensätze mit Schlüsseln, die kleiner sind als seine beiden Schlüssel -- eine für alle Datensätze, die zwischen den beiden Schlüsseln liegen -- eine für alle Datensätze mit Schlüsseln, die größer sind als seine beiden Schlüssel. - Ein 4-Knoten besitzt vier von ihm ausgehende Verkettungen, nämlich eine Verkettung für jedes der Intervalle, die durch seine drei Schlüssel definiert werden. Es ist möglich 2-3-4-Bäume als gewöhnliche binäre Bäume (mit nur zwei Knoten) darzustellen, wobei nur ein zusätzliches Bit je Knoten verwendet wird. Die Idee besteht darin, 3-Knoten und 4-Knoten als kleine binäre Bäume darzustellen, die durch „rote“ Verkettungen miteinander verbunden sind, im Gegensatz zu den schwarzen Verkettungen, die den 2-3-4-Baum zusammenhalten: oder 4-Knoten werden als 2-Knoten dargestellt, die mittels einer roten Verkettung verbunden sind. 3-Knoten werden als 2–Knoten dargestellt, die mit einer roten Markierung verbunden sind. Abb.: Rot-schwarze Darstellung von Bäumen Zu jedem 2-3-4-Baum gibt es viele ihm entsprechende Rot-Schwarz-Bäume. Diese Bäume haben eine Reihe von Eigenschaften, die sich unmittelbar aus ihrer Definition ergeben, z.B.: - Alle Pfade von der Wurzel zu einem Blatt haben dieselbe Anzahl von schwarzen Kanten. Dabei werden nur die Kanten zwischen inneren Knoten gezählt. - Längs eines beliebigen Pfads treten niemals zwei rote Verkettungen nacheinander auf.- Rot-Schwarz-Bäume erlauben es, AVL-Bäume, perfekt ausgeglichene Bäume und viele andere Klassen binärer Bäume einheitlich zu repräsentieren und zu implementieren. 61 Datenorganisation Eine Variante zu Rot-Schwarz-Bäumen Definition. Ein Rot-Schwarz-Baum ist ein Binärbaum mit folgenden Farbeigenschaften: 1. Jeder Knoten ist entweder rot oder schwarz gefärbt. 2. der Wurzelknoten ist schwarz gefärbt. 3. Falls ein Knoten rot gefärbt ist, müssen seine Nachfolger schwarz gefärbt sein. 4. Jeder Pfad von einem Knoten zu einer „Null-Referenz“ muß die gleiche Anzahl von schwarzen Knoten enthalten. Höhe. Eine Folgerung dieser Farbregeln ist: Die Höhe eines Rot-Schwarz-Baums ist etwa 2 ⋅ log( N + 1) . Suchen ist garantiert unter logN erfolgreich. Aufgabe: Ermittle, welche Gestalt jeweils ein nach den vorliegenden Farbregeln erstellte Rot-Schwarz-Baum beim einfügen folgenden Schlüssel „10 85 15 70 20 60 30 50 65 80 90 40 5 55“ annimmt 10 10 85 10 85 15 15 10 85 70 15 10 85 70 20 15 10 70 20 85 62 Datenorganisation 60 15 10 70 20 85 60 30 15 10 70 30 20 85 60 50 30 15 10 70 20 60 85 50 65 30 15 10 70 20 60 50 85 65 80 30 15 10 70 20 60 50 85 65 63 80 Datenorganisation 90 30 15 70 10 20 60 85 50 65 80 90 40 30 15 10 70 20 60 85 50 65 80 90 40 5 30 15 10 70 20 60 5 85 50 65 80 90 40 55 30 15 10 70 20 60 5 50 40 85 65 55 9 85 80 90 Die Abbildungen zeigen, daß im durchschnitt der Rot-Schwarz-Baum ungefähr die Tiefe eines AVLBaums besitzt. Der Vorteil von Rot-Schwarz-Bäumen ist der geringere Overhead zur Ausführung von Einfügevorgängen und die geringere Anzahl von Rotationen. 64 Datenorganisation „Top-Down“ Rot-Schwarz-Bäume. Kann auf dem Weg nach unten festgestellt werden, daß ein Knoten X zwei rote Nachfolgeknoten hat, dann wird X rot und die beiden „Kinder“ schwarz: X c1 X c2 c1 c2 Das führt zu einem Verstoß gegen Bedingung 3, falls der Vorgänger von X auch rot ist. In diesem Fall können aber geeignete Rotationen herangezogen werden: G P X A P S X C A G B B S C G P X S X P C A A B1 G B1 B2 B2 S C Der folgende Fall kann nicht eintreten: Der Nachbar vom Elternknoten ist rot gefärbt. Auf dem Weg nach unten muß der Knoten mit zwei roten Nachfolgern einen schwarzen Großvaterknoten haben. Methoden zur Ausführung der Rotation. /* * Interne Routine, die eine einfache oder doppelte Rotation veranlasst. * Da das Ergebnis an "parent" geheftet wird, gibt es vier Faelle. * Aufruf durch reOrientierung. * "item" ist das Datenelement reOrientierung. * "parent" ist "parent" von der wurzel des rotierenden Teilbaums. * Rueckgabe: Wurzel des rotierenden Teilbaums. */ private RotSchwarzKnoten rotation(Comparable item, RotSchwarzKnoten parent) { if (item.compareTo(parent.daten) < 0) return parent.links = item.compareTo( parent.links.daten) < 0 ? rotationMitLinksNachf(parent.links) : // LL rotationMitRechtsNachf(parent.links) ; // LR else return parent.rechts = item.compareTo(parent.rechts.daten) < 0 ? rotationMitLinksNachf(parent.rechts) : // RL rotationMitRechtsNachf(parent.rechts); // RR } /* 65 Datenorganisation * Rotation Binaerbaumknoten mit linkem Nachfolger. */ static RotSchwarzKnoten rotationMitLinksNachf( RotSchwarzKnoten k2 ) { RotSchwarzKnoten k1 = k2.links; k2.links = k1.rechts; k1.rechts = k2; return k1; } /* * Rotate Binaerbaumknoten mit rechtem Nachfolger. */ static RotSchwarzKnoten rotationMitRechtsNachf(RotSchwarzKnoten k1) { RotSchwarzKnoten k2 = k1.rechts; k1.rechts = k2.links; k2.links = k1; return k2; } Implementierung in Java15 Sie wird dadurch erschwert, daß einige Teilbäume (z.B. der rechte Teilbaum des Knoten 10 im vorliegenden Bsp.) leer sein können, und die Wurzel des Baums (, da ohne Vorgänger, ) einer speziellen Behandlung bedarf. Aus diesem Grund werden hier „Sentinel“-Knoten verwendet: - eine für die Wurzel. Dieser Knoten speichert den Schlüssel − ∞ und einen rechten Link zu dem realen Knoten - einen Nullknoten (nullNode), der eine Null-Referenz anzeigt. Der Inorder-Durchlauf nimmt aus diesem Grund folgende Gestalt an: /* Ausgabe der im Baum gespeicherten Datenelemente in sortierter Reihenfolge. */ public void printTree( ) { if( isEmpty() ) System.out.println("Leerer Baum"); else printTree( header.rechts ); } /* * Interne Methode fuer die Ausgabe des Baum in sortierter Reihenfolge. * b ist der Wurzelknoten. */ private void printTree(RotSchwarzKnoten b) { if (b != nullNode ) { printTree(b.links); System.out.println(b.daten ); printTree(b.rechts ); } } Die Klasse RotSchwarzknoten 15 vgl. Pr43220 66 Datenorganisation // BaumKnoten fuer RotSchwarzBaeume class RotSchwarzKnoten { // Instanzvariable public Comparable daten; // Dateninformation der Knoten protected RotSchwarzKnoten links; // linkes Kind protected RotSchwarzKnoten rechts; // rechtes Kind protected int farbe; // Farbe // Konstruktoren RotSchwarzKnoten(Comparable datenElement) { this(datenElement, null, null ); } RotSchwarzKnoten(Comparable datenElement, RotSchwarzKnoten l, RotSchwarzKnoten r) { daten = datenElement; links = l; rechts = r; farbe = RotSchwarzBaum.BLACK; } } Das Gerüst der Klasse RotSchwarzBaum und Initialisierungsroutinen // // // // // // // // // // // // // // Die Klasse RotSchwarzBaum Konstruktion: mit einem "negative infinity sentinel" ******************PUBLIC OPERATIONEN********************* void insert(x) --> Insert x void remove(x) --> Entferne x (nicht implementiert) Comparable find(x) --> Ruecggabe des Datenelements, das x enthaelt Comparable findMin() --> Rueckgabe des kleinsten Datenelements Comparable findMax() --> Rueckgabe des groessten Datenelements boolean isEmpty() --> Rueckgabe true, falls leer; anderenfalls false void makeEmpty() --> Entferne alles void printTree() --> Ausgabe in aufsteigend sortierter Folge void ausgRotSchwarzBaum() --> Ausgabe des Baums um 90 Grad versetzt /* * Implementierung eines RotSchwarzBaum. * Vergleiche basieren auf der Methode compareTo. */ public class RotSchwarzBaum { private RotSchwarzKnoten header; private static RotSchwarzKnoten nullNode; static // Static Initialisierer for nullNode { nullNode = new RotSchwarzKnoten(null); nullNode.links = nullNode.rechts = nullNode; } static final int BLACK = 1; // Black must be 1 static final int RED = 0; ….. /* * Baumkonstruktion. * negInf ist ein Wert, der kleiner oder gleich zu allen anderen Werten ist. */ public RotSchwarzBaum(Comparable negInf) 67 Datenorganisation { header = new RotSchwarzKnoten(negInf); header.links = header.rechts = nullNode; } ….. } Die Methode „insert“ // Fuer "insert routine" und zugehoerige unterstuetzende Routinen private static RotSchwarzKnoten current; private static RotSchwarzKnoten parent; private static RotSchwarzKnoten grand; private static RotSchwarzKnoten great; /* * Einfügen in den Baum. Duplikate werden ueberlesen. * "item" ist das einzufuegende Datenelement. */ public void insert(Comparable item) { current = parent = grand = header; nullNode.daten = item; while( current.daten.compareTo( item ) != 0 ) { great = grand; grand = parent; parent = current; current = item.compareTo(current.daten ) < 0 ? current.links : current.rechts; // Pruefe, ob zwei rote Kinder; falls es so ist, fixiere es if( current.links.farbe == RED && current.rechts.farbe == RED ) reOrientierung( item ); } // Fehlanzeige fuer Einfuegen, falls schon da if( current != nullNode ) return; current = new RotSchwarzKnoten( item, nullNode, nullNode ); // Anhaengen an die Eltern if( item.compareTo( parent.daten ) < 0 ) parent.links = current; else parent.rechts = current; reOrientierung( item ); } /* * Interne Routine, die waehrend eines Einfuegevorgangs aufgerufen wird * Falls ein Knoten zwei rote Kinder hat, fuehre Tausch der Farben aus * und rotiere. * item enthaelt das einzufuegende Datenelement. */ private void reOrientierung(Comparable item) { // Tausch der Farben current.farbe = RED; current.links.farbe = BLACK; current.rechts.farbe = BLACK; if (parent.farbe == RED) // Rotation ist noetig { grand.farbe = RED; if ( (item.compareTo( grand.daten) < 0 ) != (item.compareTo( parent.daten) < 0 ) ) parent = rotation(item, grand); // Start Doppelrotation current = rotation(item, great); current.farbe = BLACK; } header.rechts.farbe = BLACK; // Mache die Wurzel schwarz } 68 Datenorganisation Bsp.: Java-Applet16 zur darstellung eines Rot-Schwarz-Baums Abb.: 16 vgl. pr43222 69 Datenorganisation 4.3.5 AA-Bäume Die Implementierung eines Rot-Schwarz-Baums ist sehr trickreich (insbesondere das Löschen von Baumknoten). Bäume, die diese trickreiche Programmierung einschränken, sind der binäre B-Baum und der AA-Baum. Definitionen Ein BB-Baum ist ein Rot-Schwarz-Baum mit einer besonderen Bedingung: Ein Knoten hat, falls es sich ergibt, eine „roten“ Nachfolger“. Zur Vereinfachung der Implementierung kann man noch einige Regeln hinzufügen und darüber einen AA-Baum definieren: 1. Nur ein Nachfolgeknoten kann rot sein. Das bedeutet: Falls ein interner Knoten nur einen Nachfolger hat, dann muß er rot sein. Ein schwarzer Nachfolgeknoten verletzt die 4. Bedingung von Rot-Schwarz-Bäumen. Damit kann ein innerer Knoten immer durch den kleinsten Knoten seines rechten Teilbaums ersetzt werden. 2. Anstatt ein Bit zur Kennzeichnung der Färbung wird die Stufe zum jeweiligen Knoten bestimmt und gespeichert. Die Stufe eines Knoten ist - Eins, falls der Knoten ein Blatt ist. - Die Stufe eines Vorgängerknoten, falls der Knoten rot gefärbt ist. - Die Stufe ist um 1 niedriger als die seines Vorgängers, falls der Knoten schwarz ist. Eine horizontale Verkettung zeigt die Verbindung eines Knoten und eines Nachfolgeknotens mit gleicher Stufenzahl an. Der Aufbau verlangt, daß horizontale Verkettungen rechts liegen und daß es keine zwei aufeinander folgende horizontale Verkettungen gibt. Darstellung In einen zunächst leeren AA-Baum sollen Schlüssel in folgender Reihenfolge eingefügt werden: 10, 85 15, 70, 20, 60, 30, 50, 65, 80, 90, 40, 5, 55, 35. 10 10 85 10 85 15 10 15 85 „skew“ 15 „slip“ 10 85 70 Datenorganisation 70 15 10 70 20 85 15 10 70 20 85 60 15 10 70 20 60 85 30 15 10 70 20 60 85 30 15 10 30 20 60 15 85 30 20 71 70 70 60 85 Datenorganisation 30 15 10 70 20 60 85 50 30 15 10 70 20 50 60 85 65 30 15 10 60 20 70 50 65 85 80 30 15 10 60 20 70 50 65 80 90 30 70 15 10 60 20 50 72 85 65 80 90 85 Datenorganisation 40 30 70 15 60 10 20 40 85 50 65 80 90 5 30 70 15 5 60 10 20 40 85 50 65 80 90 55 30 70 15 5 10 50 20 40 60 55 85 65 80 90 35 30 15 5 10 70 20 50 35 40 Abb.: 73 60 55 85 65 - 80 90 Datenorganisation Implementierung Der Knoten des AA-Baums: // Baumknoten fuer AA-Baeume class AAKnoten { public Comparable daten; // Datenelement im Knoten protected AAKnoten links; // linkes Kind protected AAKnoten rechts; // rechtes Kind protected int level; // Level // Konstruktoren AAKnoten(Comparable datenElement) { this(datenElement, null, null); } AAKnoten(Comparable datenElement, AAKnoten l, AAKnoten r) { daten = datenElement; links = l; rechts = r; level = 1; } } Operationen an AA-Bäumen: Ein „Sentinel“ repräsentiert „null“. Suchen. Es erfolgt nach die in binären Suchbäumen üblichen Weise. Einfügen von Knoten. Es erfolgt auf der untersten Stufe (bottom level) /* * Interne Methode zum Einfuegen in einen Teilbaum. * "x" enthaelt das einzufuegende Element. * "b" ist die wurzel des baums. * Rueckgabe: die neue Wurzel. */ private AAKnoten insert(Comparable x, AAKnoten b) { if (b == nullNode) b = new AAKnoten(x, nullNode, nullNode); else if (x.compareTo(b.daten) < 0 ) b.links = insert(x, b.links); else if (x.compareTo(b.daten) > 0 ) b.rechts = insert(x, b.rechts); else return b; b = skew(b); b = split(b); return b; } /* * Skew Primitive fuer AA-Baeume. * "b" ist die Wurzel. * Rueckgabe: die neue wurzel nach der Rotation. */ private AAKnoten skew(AAKnoten b) 74 Datenorganisation { if (b.links.level == b.level ) b = rotationMitLinksNachf(b); return b; } /* * Split fuer AABaeume. * "b" ist die Wurzel. * Rueckgabe: die neue wurzel nach der Rotation. */ private AAKnoten split(AAKnoten b) { if (b.rechts.rechts.level == b.level ) { b = rotationMitRechtsNachf(b); b.level++; } return b; } Bsp.: 30 70 15 5 50 10 20 35 60 40 55 85 65 80 90 1. Einfügen des Schlüssels 2 Erzeugt wird ein linke horizontale Verknüpfung 2. Einfügen des Schlüssels 45 Erzeugt werden zwei rechte horizontale Verbindungen 30 70 15 2 5 50 10 20 35 40 60 45 55 In beiden Fällen können einfache Rotationen ausgleichen: 75 85 65 80 90 Datenorganisation - Linke horizontale Verknüpfungen werden durch ein einfache rechte Rotation behoben („skew“) X P X P C A C B A B Abb.: Rechtsrotation z.B.: 30 5 70 15 2 10 50 20 35 40 60 45 55 85 65 80 90 Abb.: - Aufeinanderfolgende rechte horizontale Verknüpfungen werden durch einfach linke Rotation behoben (split) X R G R C X A G B C A B Abb.: Linksrotation z.B. „Einfügen des Schlüssels 45“ 30 70 15 5 10 50 20 35 40 Abb.: 76 45 60 55 85 65 80 90 Datenorganisation „split“ am Knoten mit dem Schlüssel 35 30 15 5 70 40 10 20 35 50 60 45 55 85 65 80 90 „skew“ am Knoten mit dem Schlüssel 50 30 15 5 70 40 10 20 35 50 60 45 55 85 65 80 90 „split“ am Knoten mit dem Schlüssel 40 30 70 15 5 50 10 20 85 40 35 60 45 55 80 90 65 Endgültige Baumgestalt nach „skew“ am Knoten 70 und „split“ am Knoten 30 50 30 70 15 5 10 40 20 35 60 45 55 77 85 65 80 90 Datenorganisation Löschen von Knoten. /* * Interne Methode fuer das Entfernen aus einem Teilbaum. * Parameter x ist das zu entfernende Merkmal. * Parameter b ist die Wurzel des Baums. * Rueckgabe: die neue Wurzel. */ private AAKnoten remove(Comparable x, AAKnoten b) { if( b != nullNode ) { // Schritt 1: Suche bis zum Grund des Baums, setze lastNode und // deletedNode lastNode = b; if( x.compareTo( b.daten ) < 0 ) b.links = remove( x, b.links ); else { deletedNode = b; b.rechts = remove( x, b.rechts ); } // Schritt 2: Falls am Grund des Baums und // x ist gegenwaertig, wird es entfernt if( b == lastNode ) { if( deletedNode == nullNode || x.compareTo( deletedNode.daten ) != 0 ) return b; // Merkmal nicht gefunden; es geschieht nichts deletedNode.daten = b.daten; b = b.rechts; } // Schritt 3: Anderenfalls, Boden wurde nicht erreicht; Rebalancieren else if( b.links.level < b.level - 1 || b.rechts.level < b.level - 1 ) { if( b.rechts.level > --b.level ) b.rechts.level = b.level; b = skew( b ); b.rechts = skew( b.rechts ); b.rechts.rechts = skew( b.rechts.rechts ); b = split( b ); b.rechts = split( b.rechts ); } } return b; } 78 Datenorganisation 4.4 Bayer-Bäume 4.4.1 Grundlagen und Definitionen Bayer-Bäume sind für die Verwaltung der Schlüssel zu Datensätzen in umfangreichen Dateien vorgesehen. Der binäre Baum ist für die Verwaltung solcher Schlüssel nicht geeignet, da er nur jeweils einen Knoten mit einem einzigen Datensatz adressiert. Die Daten (Datensätze) stehen blockweise zusammengefaßt auf Massenspeichern, der Binärbaum müßte Knoten für Knoten auf einen solchen Block abgebildet werden. Jeder Zugriff auf den Knoten des Baums würde ein Zugriff auf den Massenspeicher bewirken. Da ein Plattenzugriff relativ zeitaufwendig ist, hätte man die Vorteile der Suchbäume wieder verloren. In einen Knoten ist daher nicht nur ein Datum aufzunehmen, sondern maximal (T - 1) Daten. Ein solcher Knoten hat T Nachfolger. Die Eigenschaften der knotenorientierten "T-ären" Intervallbäume sind : - Jeder Knoten enthält max. (T - 1) Daten - Die Daten in einem Knoten sind aufsteigend sortiert - Ein Knoten enthält maximal T Teilbäume - Die Daten (Schlüssel) der linken Teilbäume sind kleiner als das Datum der Wurzel. - Die Daten der rechten Teilbäume sind größer als das Datum der Wurzel. - Alle Teilbäume sind T-äre Suchbäume. Durch Zusammenfassen mehrerer Knoten kommt man so vom binären zum "T-ären" Suchbaum, z.B.: 8 4 12 2 1 3 5 2 3 11 9 7 8 4 1 14 10 6 5 6 15 12 9 7 13 10 11 13 14 15 Abb.: T-äre Bäume haben aber noch einen schwerwiegenden Nachteil. Sie können leicht zu entarteten Bäumen degenerieren. 79 Datenorganisation Bsp.: Ein entarteter 5-ärer Baum enthält durch Eingabe (Einfügen) der Elemente 1 bis 16 in aufsteigender Folge folgende Gestalt: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 Abb.: Durch Einfügen und Löschen können sich beliebig unsymetrische Strukturen herausbilden. Algorithmen zum Ausgleichen, vegleichbar mit den AVL-Rotationen sind jedoch nicht bekannt. Zunehmend an Bedeutung gewinnt ein höhengleicher Baum (B-Baum), der von R. Bayer eingeführt wurde. Höhengleichheit kann erreicht werden, wenn folgende Eigenschaften eingehalten werden: - Ein Knoten enthält höchstens 2 ⋅ M Schlüssel ( 2 ⋅ M + 1 -ärer Baum). Jeder Bayer-Baum (BBaum) besitzt eine Klasse, im vorliegenden Fall die Klasse M. - Jeder Knoten (Ausname: Wurzel) enthält mindestens M Schlüssel, höchstens 2 ⋅ M Schlüssel. - Jeder Nichtblattknoten hat daher zwischen ( M + 1 ) und( 2 ⋅ M + 1 ) Nachfolger. - Alle Blattknoten liegen auf der gleichen Stufe. 4.4.2 Darstellung +-----------------------------------+ | | | Z0S1Z1S2Z2S3 ........... ZN-1SNZN | | | +-----------------------------------+ Zl ... Zeiger Sl ... Schluessel Alle Schlüssel in einem Teilbaum, auf den durch Zl-1 verwiesen wird, sind kleiner als Sl. Alle Schlüssel in einem Unterbaum, auf den durch Zl verwiesen wird, sind größer als Sl. In einem B-Baum der Höhe H befinden sich daher zwischen N min = 2 ⋅ ( M + 1) H −1 − 1 und N max = 2 ⋅ ( M + 1) H − 1 Schlüssel. Neue Schlüssel werden stets in den Blättern zugefügt. 80 Datenorganisation Aufgabe: Gegeben ist die folgende Schlüsselfolge: „1, 7, 6, 2, 11, 4, 8, 13, 10, 5, 19, 9, 18, 24, 3, 12, 14, 20, 21, 17“. Bestimme die zugehörigen Strukturen eines 5-ären Baumes. 1) 1, 7, 6, 2 1 6 2 7 2) 11 6 1 2 7 11 7 8 3) 4, 8, 13 6 1 2 4 11 13 11 13 4) 10 6 1 2 10 4 7 8 5) 5, 19, 9, 18 6 1 2 4 5 7 10 8 9 11 13 11 13 18 19 6) 24 6 1 2 4 5 7 10 8 18 9 7) 3, 12, 14, 20, 21 81 19 24 Datenorganisation 1 2 4 5 3 6 7 8 10 18 11 9 12 13 14 19 20 21 24 Abb.: a) C++-Darstellung Zeiger ZI und Schlüssel SI eines jeden Knoten sind folgendermaßen angeordnet: +----------------------------- ------+ | | | Z0S0Z1S1Z2S2 ........... ZN-1SN-1ZN | | | +-------------------------------------+ Das führt zu der folgenden Beschreibung des B-Baums der Ordnung 2 (mit 5 Kettengliedern je Knoten). // // B-Baum mit bis zu MAERER Verkettungen (mit Knoten die bis zu MAERER Verkettungen enthalten) #include <iostream.h> #include <iomanip.h> #include <ctype.h> #define MAERER 5 // Anzahl Verkettungen im B-Baum: // MAERER Verkettungsfelder je Knoten typedef int dtype; enum status {unvollstaendigesEinfuegen, erfolgreich, doppelterSchluessel, Unterlauf, nichtGefunden}; struct knoten { int n; // Anzahl der Elemente, die in einem Knoten // gespeichert sind (n < MAERER) dtype s[MAERER-1]; // Datenelemente (aktuell sind n) knoten *z[MAERER]; // Zeiger auf andere Knoten (aktuell sind n+1) }; // Logische Ordnung: // z[0], s[0], z[1], s[1], ..., z[n-1], s[n-1], z[n] class Bbaum { private: knoten *wurzel; status einf(knoten *w, dtype x, dtype &y, knoten* &q); void ausg(const knoten* w, int nLeer)const; int knotenSuche(dtype x, const dtype *a, int n)const; status loe(knoten *w, dtype x); public: Bbaum(): wurzel(NULL){} void einfuegen(dtype x); void gibAus()const{cout << "Dateninhalt:\n"; ausg(wurzel, 0);} void loeschen(dtype x); void zeigeSuche(dtype x)const; }; 82 Datenorganisation b) Java-Darstellung Die Klasse CBNode zeigt die Implementierung eines Bayer-Baumknotens17. Der Konstruktor dieser Klasse leistet die Arbeit und bekommt dazu einen Übergabeparameter (M) geliefert, der die Ordnung des (M-ären) Bayer-Baums beschreibt. import java.util.*; /* * The CBNode class represents on single Node of the Bayer Tree. */ public class CBNode { // Instanzvariable protected Vector key, nodeptr, initkey, initvec; int count; // Konstruktor /* * constructs a single Bayer Tree node with M references to subnodes. */ CBNode(int M) { // System.out.println("CBNode(): constructor invoked!"); nodeptr = new Vector(); initvec = new Vector(); initvec = null; key = new Vector(); initkey = new Vector(); initkey = null; for(int i = 0; i <= M; i++) { nodeptr.addElement((Object)initvec); // System.out.println(i + "ter Nodepointer erzeugt. Wert: " + nodeptr.elementAt(i)); } for(int j = 0; j <= M - 1; j++) { key.addElement((Object)initkey); // System.out.println(j + "ter Key erzeugt. Wert: " + key.elementAt(j)); } count = 0; // System.out.println("count Wert: " + count); } } 17 Vgl. pr44200, CBNode.java 83 Datenorganisation 4.4.3 Suchen eines Schlüssels Gegeben ist der folgende Ausschnitt eines B-Baums mit N Schlüsseln: +----------------------------- ------+ | | | Z0S0Z1S1Z2S2 ........... ZN-1SN-1ZN | | | +-------------------------------------+ S0<S1<S2<.....<SN-1 Handelt es sich beim Knoten um ein Blatt, dann ist der Wert eines jeden Zeigers ZI im Knoten NULL. Falls der Knoten kein Blatt ist, verweisen einige der (N+1) Zeiger auf andere Knoten (Kinder des aktuellen Knoten). I > 0: Alle Schlüssel im betreffenden Kind-Knoten, auf den ZI zeigt, sind größer als SI-1 I < N: Alle Schlüssel im betreffenden Kind, auf das ZI zeigt, sind kleiner als SN. Hat einer der angegebenen Zeiger den Wert „null“, dann existiert in dem vorliegenden Baum kein Teilbaum, der diesen Schlüssel enthält (d.h. die Suche ist beendet). a) C++-Implementierung int Bbaum::knotenSuche(dtype x, const dtype *a, int n)const { int i=0; while (i < n && x > a[i]) i++; return i; } void Bbaum::zeigeSuche(dtype x)const { cout << "Suchpfad:\n"; int i, j, n; knoten *w = wurzel; while (w) { n = w->n; for (j = 0; j < w->n; j++) cout << cout << endl; i = knotenSuche(x, w->s, n); if (i < n && x == w->s[i]) { cout << "Schluessel " << x << " << " vom zuletzt angegebenen return; } w = w->z[i]; } cout << "Schluessel " << x << " wurde } " " << w->s[j]; wurde in Position " << i Knoten gefunden.\n"; nicht gefunden.\n"; b) Java-Implementierung /* *Rueckgabe ist true, falls der Schlüssel gefunden wurde; * anderenfalls false. */ public boolean searchkey(int key) { boolean found = false; int i = 0, n; 84 Datenorganisation CBNode node = root; while(node != null) { i = 0; n = node.count; // search key in actual node while(i < n && key > ((Integer)node.key.elementAt(i)).intValue()) { i++; } // end while if(i < n && key == ((Integer)node.key.elementAt(i)).intValue()) { found = true; } if(node.nodeptr.elementAt(i) != null) { mpCVisualizeTree.MoveLevelDown(i); } node = (CBNode) node.nodeptr.elementAt(i); } // end while (node != null) return found; } // end searchkey Methode 4.4.4 Einfügen Bsp.: Der Einfügevorgang in einem Bayer-Baum der Klasse 2 1) Aufnahme der Schlüssel 1, 2, 3, 4 in den Wurzelknoten 1 3 2 4 2) Zusätzlich wird der Schlüssel mit dem Wert 5 eingefügt 1 3 2 4 5 Normalerweise würde jetzt rechts von "4" ein neuer Knotem erzeugt. Das würde aber zu einem Knotenüberlauf führen. Nach dieser Erweiterung enthält der Knoten eine ungerade Zahl von Elementen ( 2 ⋅ M + 1 ). Dieser große Knoten kann in 2 Söhne zerlegt werden, nur das mittlere Element verbleibt im Vaterknoten. Die neuen Knoten genügen wieder den B-Baum-Eigenschaften und könnem weitere Daten aufnehmen. 3 1 2 4 5 Abb.: Beschreibung des Algorithmus für das Einfügen Ein neues Element wird grundsätzlich in einen Blattknoten eingefügt. Ist der Knoten mit 2 ⋅ M Schlüsseln voll, so läuft bei der Aufnahme eines weiteren Schlüssels der Knoten über. 85 Datenorganisation +---------------------------------------------+ | | | ........... SX-1ZX-1SXZX ..... | | | +---------------------------------------------+ +----------------------------------------------+ | | | Z0S1Z1 .... ZM-1SMZMSM+1 ....... Z2MS2M+1 | | Überlauf | +----------------------------------------------+ Abb.: Der Knoten wird geteilt: Die vorderen Schlüssel verbleiben im alten Knoten, der Schlüssel mit der Nummer M+1 gelangt als Trennschlüssel in den Vorgängerknoten. Die M Schlüssel mit den Nummern M+2 bis 2 ⋅ M + 1 kommen in den neuen Knoten. +-----------------------------+ | ....SX-1ZX-1SM+1ZYSXZX .... | +-----------------------------+ +------------------+ |Z0S1 .... ZM-1SMZM| +------------------+ +-----------------------------+ |ZM+1SM+2ZM+2 ..... S2M+1Z2M+1| +-----------------------------+ Abb.: Die geteilten Knoten enthalten genau M Elemente. Das Einfügen eines Elements in der vorangehenden Seite kann diese ebenfalls zum Überlaufen bringen und somit die Aufteilung fortsetzen. Der B-Baum wächst demnach von den Blättern bis zur Wurzel. a) Methoden zum Einfügen der Klasse Bbaum (in C++) Zwei Funktionen (Methoden der Klasse Bbaum) „einfuegen“ und „einf“ teilen sich die Arbeit. Der Aufruf dieser Funktionen erfolgt bspw. über b.einfuegen(x)18; bzw. in der Methode einfuegen() über status code = einf(wurzel, x, xNeu, zNeu);. xNeu, zNeu sind lokale Variable in einfuegen(). Die private Methode einf() fiefert einen „code“ zurück: unvollstaendigesEinfuegen , falls (insgesamt gesehen) der Einfügevorgang noch nicht vollständig abgeschlossen wurde. erfolgreich , falls das Einfügen des Schlüssels x erfolgreich war doppelterSchluessel , falls x bereits im Bayer-Baum ist. In den „code“-Fällen „erfolgreich“ bzw. „doppelterSchluessel“ haben xNeu und zNeu keine Bedeutung. Im Fall „unvollstaendigesEinfuegen“ sind noch weitere Vorkehrungen zu treffen: - Falls überhaupt noch kein Bayer-Baum vorliegt (wurzel == NULL) ist ein neuer Knoten zu erzeugen und der Wurzel zuzuordnen. In diesem Fall enthält der Wurzelknoten dann einen Schlüssel. 18 „b“ ist eine Instanz von Bbaum 86 Datenorganisation - In der Regel tritt unvollständiges Einfügen auf, wenn der Knoten, in den der Schlüssel x eingefügt werden soll, keinen Platz mehr besitzt. Ein Teil der Schlüssel kann im alten Knoten verbleiben, der andere Teil muß in einen neuen Knoten untergebracht werden. void Bbaum::einfuegen(dtype x) { knoten *zNeu; dtype xNeu; status code = einf(wurzel, x, xNeu, zNeu); if (code == doppelterSchluessel) cout << "Doppelte Schluessel werden ignoriert.\n"; if (code == unvollstaendigesEinfuegen) { knoten *wurzel0 = wurzel; wurzel = new knoten; wurzel->n = 1; wurzel->s[0] = xNeu; wurzel->z[0] = wurzel0; wurzel->z[1] = zNeu; } } status Bbaum::einf(knoten *w, dtype x, dtype &y, knoten* &q) { // Fuege x in den aktuellen Knoten, adressiert durch *this ein. // Falls nicht voll erfolgreich, sind noch Ganzzahl y und Zeiger q // einzufuegen. // Rueckgabewert: // erfolgreich, doppelterSchluessel oder unvollstaendigesEinfuegen. knoten *zNeu, *zFinal; int i, j, n; dtype xNeu, sFinal; status code; if (w == NULL){q = NULL; y = x; return unvollstaendigesEinfuegen;} n = w->n; i = knotenSuche(x, w->s, n); if (i < n && x == w->s[i]) return doppelterSchluessel; code = einf(w->z[i], x, xNeu, zNeu); if (code != unvollstaendigesEinfuegen) return code; // Einfuegen im untergeordneten Baum war nicht voll erfolgreich; // Versuch zum Einfuegen in xNeu und zNeu vom aktuellem Knoten: if (n < MAERER - 1) { i = knotenSuche(xNeu, w->s, n); for (j=n; j>i; j--) { w->s[j] = w->s[j-1]; w->z[j+1] = w->z[j]; } w->s[i] = xNeu; w->z[i+1] = zNeu; ++w->n; return erfolgreich; } // Der aktuelle Knoten ist voll (n == MAERER - 1) und muss gesplittet // werden. // Reiche das Element s[h] in der Mitte der betrachteten Folge zurueck // ueber Parameter y, damit es aufwaerts im Baum plaziert werden kann. // Reiche auch einen Zeiger zum neu erzeugten Knoten zurueck (als // Verweis) ueber den Parameter q: if (i == MAERER - 1) {sFinal = xNeu; zFinal = zNeu;} else { sFinal = w->s[MAERER-2]; zFinal = w->z[MAERER-1]; for (j=MAERER-2; j>i; j--) { w->s[j] = w->s[j-1]; w->z[j+1] = w->z[j]; } w->s[i] = xNeu; w->z[i+1] = zNeu; } int h = (MAERER - 1)/2; y = w->s[h]; // y und q werden zur naechst hoeheren Stufe q = new knoten; // im Baum weitergereicht // Die Werte z[0],s[0],z[1],...,s[h-1],z[h] gehoeren zum linken Teil von // s[h] and werden gehalten in *w: w->n = h; // z[h+1],s[h+1],z[h+2],...,s[MAERER-2],z[MAERER-1],sFinal,zFinal // gehoeren zu dem rechten Teil von s[h] und werden nach *q gebracht: 87 Datenorganisation q->n = MAERER - 1 - h; for (j=0; j < q->n; j++) { q->z[j] = w->z[j + h + 1]; q->s[j] = (j < q->n - 1 ? w->s[j + h + 1] : sFinal); } q->z[q->n] = zFinal; return unvollstaendigesEinfuegen; } b) Methoden zum Einfügen in Java /* *Einfügen eines neuen Schlüssels. Rückgabe ist –1, falls * es misslingt, anderenfalls 0 * Parameter: value einzufuegender Wert */ public int Insert(Integer value) { if(root == null) { root = new CBNode(MAERER); root.key.setElementAt((Object)value, 0); root.count = 1; } // end if else { if(searchkey(((Integer)value).intValue()) == true) { //System.out.println("double key found and will be ignored!"); return -1; } // end if CBNode result; result = insrekurs(root, value); if(result != null) { CBNode node = new CBNode(MAERER); node.key.setElementAt(newValue, 0); node.nodeptr.setElementAt(root, 0); node.nodeptr.setElementAt(result, 1); node.count = 1; root = node; } // end if(result) } // end else mpCVisualizeTree.DeleteRootKnot(); mpCExtendedCanvas.repaint(); rootflag = 0; this.drawTree(root); mpCExtendedCanvas.repaint(); return 0; } // end Insert() Methode Die Methode „Insert“ ruft „insrekurs()“ auf. Diese rekursive Methode leistet die eigentliche Arbeit /* * der neue Wert wird rekursiv in den Baum eingebracht */ protected CBNode insrekurs(CBNode tempnode, Integer insValue) { CBNode result; result = null; newValue = insValue; if(tempnode.nodeptr.elementAt(0) != null) // kein Blatt -> Rekursion { 88 Datenorganisation int pos = 0; while(pos < tempnode.count && newValue.intValue() > ((Integer)tempnode.key.elementAt(pos)).intValue()) { pos++; } // end while result = insrekurs((CBNode)tempnode.nodeptr.elementAt(pos), newValue); if(result == null) { return null; // if result = null: nothing has to be inserted into // node-> finished! } // end if(resul == null) } // end if(tempnode.nodeptr.elementAt(0) != null) // insert a element CBNode node = null; int flag = 0; int s = tempnode.count; if(s >= MAERER - 1) // split the knot { tempnode.count = (MAERER - 1) / 2; node = new CBNode(MAERER); node.count = (MAERER - 1) / 2; for(int d = ((MAERER - 1) / 2); d > 0;) { if(flag != 0 || ((Integer)tempnode.key.elementAt(s - 1)).intValue() > newValue.intValue()) { node.nodeptr.setElementAt(tempnode.nodeptr.elementAt(s), d); node.key.setElementAt(tempnode.key.elementAt(--s), --d); } // end if(flag != 0 ... else { node.nodeptr.setElementAt(result, d); node.key.setElementAt(newValue, --d); flag = 1; } // end else } // end if(s >= MAERER - 1) if(flag != 0 || ((Integer)tempnode.key.elementAt(s - 1)).intValue() > newValue.intValue()) { node.nodeptr.setElementAt(tempnode.nodeptr.elementAt(s), 0); } // end if else { node.nodeptr.setElementAt(result, 0); } // end else } // end if(s >= MAERER - 1) else { tempnode.count++; } // end else // shift for(; s > 0 && ((Integer)tempnode.key.elementAt(s - 1)).intValue() > newValue.intValue(); s--) { tempnode.nodeptr.setElementAt(tempnode.nodeptr.elementAt(s), s + 1); tempnode.key.setElementAt(tempnode.key.elementAt(s - 1), s); } // end for tempnode.key.setElementAt(newValue, s); tempnode.nodeptr.setElementAt(result, s + 1); newValue = (Integer) tempnode.key.elementAt((MAERER - 1) / 2); return node; } // end insrekurs() methode 89 Datenorganisation 4.4.5 Löschen Grundsätzlich ist zu unterscheiden: 1. Das zu löschende Element ist in einem Blattknoten 2. Das Element ist nicht in einem Blattknoten enthalten. In diesem Fall ist es durch eines der benachbarten Elemente zu ersetzen. Entlang des rechts stehenden Zeigers Z ist hier zum Blattknoten hinabzusteigen und das zu löschende Element durch das äußere linke Element von Z zu ersetzen. Auf jeden Fall darf die Anzahl der Schlüssel im Knoten nicht kleiner als M werden. Ausgleichen Die Unterlauf-Gegebenheit (Anzahl der Schlüssel ist kleiner als M) ist durch Ausleihen oder "Angliedern" eines Elements von einem der benachbarten Knoten abzustellen. Zusammenlegen Ist kein Element zum Angliedern übrig (, der benachbarte Knoten hat bereits die minimale Größe erreicht), dann enthalten die beiden Knoten je 2 ⋅ M − 1 Elemente. Beide Knoten können daher zusammengelegt werden. Das mittlere Element ist dazu aus den dem Knoten vorausgehenden Knoten zu entnehmen und der NachbarKnoten ist ganz zu entfernen. Das Herausnehmen des mittleren Schlüssels in der vorausgehenden Seite kann nochmals die Größe unter die erlaubte Grenze fallen lassen und gegebenenfalls auf der nächsten Stufe eine weitere Aktion hervorrufen. 90 Datenorganisation Bsp.: Gegeben ist ein 5-ärer B-Baum (der Ordnung 2) in folgender Gestalt: 50 30 10 20 35 40 60 42 38 44 46 56 58 65 80 70 90 95 1) Löschen der Schlüssel 44, 80 50 30 10 20 35 40 38 90 60 42 56 46 58 65 70 95 96 Abb.: 2) Einfügen des Schlüssels 99, Löschen des Schlüssels 70 mit Ausgleichen 50 30 10 20 35 40 38 95 60 42 56 46 Abb.: 3) Löschen des Schlüssels 35 mit Zusammenlegen 91 58 65 90 95 99 96 Datenorganisation 50 40 10 30 20 38 42 56 46 40 10 20 30 38 42 58 60 50 46 56 60 95 65 90 95 99 95 58 65 90 95 a) Implementierung in C++ Löschen eines Schlüssels im Blattknoten 1. Fall: Im Blattknoten befinden sich mehr als die kleinste zulässige Anzahl von Schlüsselelementen. Der Schlüssel kann einfach entfernt werden, die rechts davon befindlichen Elemente werden einfach eine Position nach links verschoben. 2. Fall: Das Blatt enthält genau nur noch die kleinste zulässige Anzahl von Schlüsselelementen, Nachbachknoten auf der Ebene der Blattknoten enthalten mehr als die kleinste zulässige Anzahl von Schlüsselelementen. Der Schlüssel wird gelöscht, im Blatt liegt dann ein „Unterlauf“ vor. Man versucht aus den linken oder rechten Nachbarknoten ein Element zu besorgen, z.B.: Es liegt im Anschluß an einen (rekursiven) Aufruf, der in einem Blattknoten einen Schlüssel entfernt hat, folgende Situation vor: 20 10 12 15 30 40 25 33 34 36 46 48 Abb.: Die Entnahme geeigneter Schlüssel kann hier aus dem linken bzw. aus dem rechten Nachbarn vom betroffenen Knoten erfolgen: 92 99 Datenorganisation 20 10 12 33 15 40 25 30 34 36 46 48 Abb.: Falls vorhanden, soll immer der rechte Nachbarknoten gewählt werden. Im vorliegenden Beispiel ist das nicht möglich beim Löschen der Schlüsselwerte „46“ bzw. „48“. In solchen Fällen wird dem Linken Knoten ein Element entnommen. 3. Fall: Das Blatt enthält genau die kleinste mögliche Anzahl an Elementen, Nachbarknoten auf der Ebene der Blattknoten enthalten auch nur genau die kleinste mögliche Anzahl an Elementen. In diesem Fall müssen die betroffenen Knoten miteinander verbunden werden, z.B. liegt im Anschluß an einen Aufruf, der in einem Blattknoten einen Schlüssel entfernt hat, folgende Situation vor: 20 10 12 30 15 40 25 34 36 46 48 Die Verbindung zu einem Knoten mit zulässiger anzahl von Schlüsselelementen kann so vollzogen werden: 20 10 12 40 15 25 30 34 36 46 48 Abb.: Löschen eines Schlüssels in einem inneren Knoten Solches Löschen kann aus einem Löschvorgang in einem Blattknoten resultieren. Bsp.: 15 3 1 2 4 5 6 20 10 12 18 19 Abb.: Das Löschen vom Schlüssel mit dem Wert 1 ergibt: 93 22 30 60 70 80 Datenorganisation 6 2 3 4 5 10 15 12 20 60 18 19 22 30 70 80 Abb.: Im übergeordneten Knoten kann es erneut zu einer Unterlauf-Gegebenheit kommen. Es ist wieder ein Verbinden bzw. Borgen mit / von Nachbarknoten erforderlich, bis man schließlich an der Wurzel angelangt ist. Im Wurzelknoten kann nur ein Element sein. Wird dieses Element in den Verknüpfungsvorgang der beiden unmittelbaren Nachfolger einbezogen, dann wird der Wurzelknoten gelöscht, die Höhe des Baums nimmt ab. Eine Löschoperation kann auch direkt in einem internen Knoten beginnen, z.B. wird im folgenden Bayer-Baum im Wurzelknoten der Schlüssel mit dem Wert „15“ gelöscht. 15 3 1 2 4 6 5 20 10 12 18 19 60 22 30 70 80 Abb.: Zuerst wird zum linken Nachfolger gewechselt, anschließend wird der Baum bis zum Blatt nach rechts durchlaufen. In diesem Blatt wird dann das am weitesten rechts stehenden Datum aufgesucht und mit dem zu löschenden Element im Ausgangsknoten getauscht. 12 3 1 2 4 6 5 20 10 15 18 19 22 30 60 70 80 Abb.: Der Schlüssel mit dem Wert „15“ kann jetzt nach einer bereits beschriebenen Methode gelöscht werden. 94 Datenorganisation 4.4.6 Auf Platte/ Diskette gespeicherte Datensätze Der Ausgangspunkt zu B-Bäumen war die Verwaltung der Schlüssel zu Datensätzen in umfangreichen Dateien. In der Regel will man ja nicht nur einfache Zahlen (d.h. einzelne Daten), sondern ganze Datensätze speichern. Eine größere Anzahl von Datensätzen einer solchen Datei ist aber im Arbeitsspeicher (, der ja noch Teile des Betriebssystems, das Programm etc. enthalten muß,) nicht unterzubringen. Notwendig ist die Auslagerung von einem beträchtlichen Teil der Datensätze auf einen externen Speicher. Dort sind die Datensätze in einer Datei gespeichert und in "Seiten" zusammengefaßt. Eine Seite umfaßt die im Arbeitsspeicher adressierbare Menge von Datensätzen (Umfang entspricht einem Bayer-Baumknoten). Aus Vergleichsgründen soll hier die Anzahl der aufgenommenen bzw. aufzunehmenden Datensätze die Zahl M = 2 nicht überschreiten. Es werden also mindestens 2, im Höchstfall 4 Datensätze in eine Seite aufgenommen. Allgemein gilt: Je größer M gewählt wird, umso mehr Arbeitsspeicherplatz wird benötigt, um so größer ist aber auch die Verarbeitungsleistung des Programms infolge der geringeren Anzahl der (relativ langsamen) Zugriffsoperationen auf externe Speicher. Die 1. Seite der Datei (Datenbank) enthält Informationen für die Verwaltung. Implementierung in C++ Die Verwaltung eines auf einer Datei hinterlegten Bayer-Baums übernimmt die folgende Klasse BBaum: #define MAERER 5 // Anzahl Verkettungen im Bayer-Baum Knoten enum status {unvollstaendigesEinfuegen, erfolgreich, doppelterSchluessel, Unterlauf, nichtGefunden}; typedef int dtype; // Knoten eines auf einer Datei hinterlegten Bayer-Baums. struct knoten { int n; // Anzahl der Elemente, die in einem Knoten // Knoten gespeichert sind (n < MAERER) dtype s[MAERER-1]; // Datenelemente (aktuell sind n) long z[MAERER]; // 'Zeiger' auf andere Knoten (aktuell sind n+1) }; // Logische Ordnung: // z[0], s[0], z[1], s[1], ..., z[n-1], s[n-1], z[n] // Die Klasse zum auf einer Datei hinterlegten Bayer-Baum class BBaum { private: enum {NIL=-1}; long wurzel, freieListe; knoten wurzelKnoten; fstream datei; status einf(long w, dtype x, dtype &y, long &u); void ausg(long w, int nLeer); int knotenSuche(dtype x, const dtype *a, int n)const; status loe(long w, dtype x); void leseKnoten(long w, knoten &Knoten); void schreibeKnoten(long w, const knoten &Knoten); 95 Datenorganisation void leseStart(); long holeKnoten(); void freierKnoten(long w); public: BBaum(const char *BaumDateiname); ~BBaum(); void einfuegen(dtype x); void einfuegen(const char *eingabeDateiname); void gibAus(){cout << "Dateninhalt:\n"; ausg(wurzel, 0);} void loeschen(dtype x); void zeigeSuche(dtype x); }; Konstruktoren Zur Verwaltung des Bayer-Baums in einer Datei ist besonders wichtig: - Die Wurzel wurzel (d. h. die Position des Wurzelknotens - eine Liste mit Informationen über den freien Speicherplatz in der Datei (adressiert über freieListe). Zu solchen freien Speicherplätzen kann es beim Löschen von Schlüsselelementen kommen. Zweckmäßigerweise wird dann dieser freie Speicherbereich nicht aufgefüllt, sondern in einer Liste freier Speicherbereiche eingekettet. freieListe zeigt auf das erste Element in dieser Liste. Solange das Programm läuft sind „wurzel“ und „freieListe“ Datenelemente der Klasse BBaum (in einer Datei abgelegter Bayer-Baum). Für die Belegung dieser Dateielemente wird am Dateianfang (1.Seite) Speicherplatz reserviert. Am Ende der Programmausführung werden die Werte zu „wurzel“ bzw. „freieListe“ in der Datei abgespeichert. Bsp.: Der folgende Bayer-Baum 20 10 15 60 80 10 wurzel 15 60 80 20 freieListe Abb.: Zeiger haben hier ganzzahlige Werte des Typs long (mit -1L als NIL), die für die Positionen und Bytenummern stehen. 96