FB Informatik Prof. Dr. R.Nitsch Programmieren - Nichtlineare Datenstrukturen Reiner Nitsch [email protected] Nichtlineare Dynamische Datenstrukturen Operation Suchen Einfügen Löschen Auswählen Sort. Array O(log N) O(N) O(N) O(1) Sortierte physikalische Sequenzen (z.B. C-Array, vector, deque, …) sind schnell beim Auswählen (k-tes Element z.B. über Index) bzw. Suchen (binäre Suche nach bestimmtem Wert) aber langsam beim Einfügen bzw. Löschen. 19.01.2011 Sort. Liste O(N) O(1) O(1) O(N) FB Informatik Prof. Dr. R.Nitsch Bäume O(log N) O(1) O(1) O(log N) Bei den logischen Sequenzen (z.B. sortierte Liste) sind die Verhältnisse genau umgekehrt! Nichtlineare Datenstruktur Baum Gesucht: Datenstruktur, die die Vorteile von Liste und Array vereint, also schnelles Suchen, Einfügen und Löschen! Lösung: Bäume (Trees)! 2 Datenstruktur Baum - Terminologie und Definitionen FB Informatik Prof. Dr. R.Nitsch Bäume als wichtige Teilklasse von Graphen: Ein Baum ist ein einfacher, azyklischer, zusammenhängender Graph mit ungerichteten Kanten bei n Knoten hat er genau n-1 Kanten Es gibt genau einen Pfad, der 2 Knoten verbindet Knoten Kante Orientierter Baum (Wurzelbaum) (ungerichteter) Baum Es gibt einen ausgezeichneten Knoten, den man Wurzel nennt Die Wurzel bestimmt implizit die Orientierung der Kanten (alle zur Wurzel hin oder von der Wurzel weg gerichtet) d Wurzel (root) Beispiel rechts: d ist die Wurzel d ist Elternknoten (Vorgänger) von b, k und e b ist Kind (Nachfolger) von d a b, k und e haben denselben Elternknoten und sind daher Geschwister k b e c g f 19.01.2011 Nichtlineare Datenstruktur Baum h 3 Datenstruktur Baum - Terminologie und Definitionen Der Grad eines Knotens (die Ordnung) ist durch die Anzahl seiner Kinder bestimmt. Knoten ohne Kinder (Grad 0) heißen Blatt. (a,c,k,f,h) Alle anderen Knoten heißen innere Knoten (b,d,e,g) Jeder Knoten mit Ausnahme der Wurzel hat genau einen Elternknoten. Der Grad (die Ordnung) eines Baumes ergibt sich aus dem maximalen Grad aller Knoten (manchmal auch auf Englisch "fan-out" genannt) Die Nachfahren eines Knotens v sind alle Nachfolger und Nachfolger von Nachfolgern usw. 19.01.2011 FB Informatik Prof. Dr. R.Nitsch Grad=3 d Wurzel (root) Grad=1 k b a e c Blatt g f Grad=2 h Wurzelbaum vom Grad 3 Nichtlineare Datenstruktur Baum 4 Datenstruktur Baum - Terminologie und Definitionen Pfadlänge eines Weges: Zahl der Kanten auf dem Weg von der Wurzel zum Knoten. Die Ebene (oder Stufe) eines Knotens v ist die Länge des Pfades von der Wurzel zu v: Ebene der Wurzel = 0. Ebene eines Knotens v = (Ebene des Vorgängers von v)+1. Ebene 0 d k b a Nichtlineare Datenstruktur Baum 1 e c Die Höhe des Baumes ist die maximale Ebene seiner Knoten. Das Gewicht eines Baumes ist die Anzahl seiner Blätter. 19.01.2011 FB Informatik Prof. Dr. R.Nitsch g f 2 i 3 Höhe = 3 5 Binärer Baum und binärer Suchbaum (BST: Binary Search Tree) Wichtigster Sonderfall: Binärbaum = Wurzelbaum mit Grad 2. Definition eines binären Baums: Ein binärer Baum ist entweder ein leerer Baum oder ein Knoten, der mit einem Paar von binären Bäumen verbunden ist. Diese beiden Bäume bezeichnet man als linken und rechten Teilbaum. (Teil-)Bäume können auch leer sein. Definition des binären Suchbaums (BST): Knoten enthalten einen eindeutigen Schlüsselwert. Ein binärer Suchbaum ist ein binärer Baum, bei dem die Knoten des linken Teilbaums eines Knotens nur kleinere Schlüsselwerte und die Knoten des rechten Teilbaums eines Knotens nur größere oder gleichgroße Werte Schlüsselwerte als der Knoten selbst besitzen (soweit es keine leeren Teilbäume sind). 19.01.2011 d Wurzel (root) e b a FB Informatik Prof. Dr. R.Nitsch c g Linker Teilbaum f Binärer Baum und Binärer Suchbaum Nichtlineare Datenstruktur Baum h Rechter Teilbaum d NULL Links Leere Teilbäume (Manchmal auch als externe Knoten bezeichnet) 6 Vollständiger und ausgeglichener binärer Baum FB Informatik Prof. Dr. R.Nitsch Ein vollständiger Binärbaum enthält die für seine Höhe maximale Anzahl von Knoten. Genauere Definition: Ein vollständiger Binärbaum der Höhe h hat folgende Eigenschaften: jeder Knoten der Ebene h ist ein Blatt jeder Knoten der Ebene i < h hat genau 2 Nachfolger 1 f b a c e h=2 g vollständiger binärer Baum Definition: Bei einem ausgeglichenen 0 f Binärbaum existiert ein k 0, so daß jedes Blatt auf Ebene k oder k + 1 ist und jeder Knoten auf einer kleineren Stufe als k hat Grad 2. Definition: Ein entarteter Binärbaum liegt vor, wenn jeder Elternknoten nur einen Kindknoten hat 0 d 1 h b d a c g e j e 2 2 ausgeglichener binärer Baum 19.01.2011 Nichtlineare Datenstruktur Baum 7 Metriken und Eigenschaften von BST FB Informatik Prof. Dr. R.Nitsch Maximale Anahl von Knoten Ebene auf Ebene i: 2i für i ≥ 0 im vollständigen Binärbaum: 2h+1-1 Die max. Pfadlänge im BST ist identisch mit seiner Höhe h. 0 Die max. Pfadlänge vollständiger oder ausgeglichener Binärbaume mit n Knoten ist vorhersagbar: max. Pfadlänge = h = int(log2n) b 1 e 1 1 a c g 2 2 2 Pfadlängen 19.01.2011 0 d Nichtlineare Datenstruktur Baum 2 f i 3 3 3 Höhe = 3 8 Zeitkomplexitäten der Operationen im (ausgeglichenen) BST Wie sucht man nach einem Schlüsselwert (z.B.33)? FB Informatik Prof. Dr. R.Nitsch Eingefügt werden die Schlüsselwerte: 80 47 96 20 15 60 25 42 73 23 33 37 28 Eine erfolgreiche Suche (Suchtreffer) endet am gesuchten Knoten (Suchschlüssel). Der Suchaufwand (= Anzahl Vergleiche) ist proportional zur Pfadlänge+1 < int(log2n)+1 O(log n) 80 Woran erkennt man, ob ein Suchschlüssel nicht existierte (z.B. 79)? Eine vergebliche Suche (Suchfehler) endet immer an einem leeren Teil-Baum (erkennbar am NULL-Link zum Nachfolger). Leere Teilbäume sind im Beispiel durch Rechtecke (externe Knoten) dargestellt. Aufwand wie bei erfolgloser Suche nach externem Knoten: Pfadlänge zum externen Knoten+1 < int(log2n)+2 O(log n) Wie fügt man im BST ein (z.B. Schlüsselwert 79)? Erfolgreiches Einfügen (keine Duplikate) beginnt wie eine erfolglose Suche. Am Endpunkt der Suche, wird der externe Knoten (NULL-Link) durch den einzufügenden Knoten ersetzt. Aufwand wie bei erfolgloser Suche nach externem Knoten: Pfadlänge zum externen Knoten+1 < int(log2n)+2 O(log n) Die Einfügereihenfolge ist nicht rekonstruierbar! Wie löscht man im BST einen Knoten (z.B. 37)? Löschen setzt eine erfolgreiche Suche voraus. Das Löschen selbst ist von konstanter Laufzeit: O(1) (kommt später) Ein entarteter Baum entspricht einer Liste 47 20 15 Nichtlineare Datenstruktur Baum 60 25 23 73 42 33 28 37 Ergänzendes Beispiel: n = 1 000 000 mittl. Anzahl Vergleiche bei Suche in verketteter Liste: 500 000 in BST: 20 Aufwände siehe Liste: Suchen und Auswählen O(1); Einfügen und Löschen: O(N) 19.01.2011 96 9 Leistungsmerkmale von BST FB Informatik Prof. Dr. R.Nitsch Der schnellste Suchalgorithmus, die "Binäre Suche" benötigt ld(N) Vergleiche im Mittel. Sie setzt (sortierte) physikalische Sequenzen voraus, die Einfüge- und Löschoperationen aber nur mit O(N) zulassen . Die Suche in einem aus zufälligen Werten aufgebauten und nicht ausgeglichenen BST benötigt im Mittel 1,39•ld(N) Vergleiche also O( ld N ) Die Suche im BST ist damit nur um etwa 40% aufwändiger als die binäre Suche. Der Aufwand für die Einfüge- und Löschoperationen ist unabhängig von der Knotenanzahl O(1) (wie bei logischer linearer Sequenz). Beispiel eines Binären Suchbaums: Enstanden durch Einfügen von etwa 200 zufälligen Werten in einen anfangs leeren Baum. Eine Suche benötigt im Durchschnitt 10 Vergleiche, nie mehr als 12 Vergleiche. In einer 200-elementigen Liste wird man im Mittel erst nach 100 Vergleichen fündig! 19.01.2011 Nichtlineare Datenstruktur Baum 11 Verständnisfragen FB Informatik Prof. Dr. R.Nitsch Um welchen Baumtyp handelt es sich? K Wieviele Knoten? Wieviele Blätter? Welche Höhe? Vollständig? Ausgeglichen? Entartet? 19.01.2011 F C A Nichtlineare Datenstruktur Baum L H D M S O X Y U 12 Interne Darstellung eines Binären Suchbaums FB Informatik Prof. Dr. R.Nitsch In Programmiersprachen mit Pointern bzw. Referenzen (C, C++, Java, …): Knotenobjekte (z.B. Typ Node) enthalten d Schlüsselwert und ggf zusätzliche assoziierte Daten 2 Pointer/Referenzen auf die Nachfolger ggf zusätzlich 1 Pointer auf den Vorgänger e b a Wurzel (root) c g f h Binärer Suchbaum parent struct Node { Node( const K& t ) :key(t),left(0),right(0),parent(0) { } ~Node() { } Node *left, *right, *parent; K key; parent key }; key K left right Datenstruktur in struct Node parent K left right 19.01.2011 Nichtlineare Datenstruktur Baum key K left right 13 Implementierung eines BST Containers: class Tree FB Informatik Prof. Dr. R.Nitsch typedef Any K; struct Node root class Tree { // Implementation erlaubt keine Duplikate lmost parent struct Node { /* siehe oben (eingeschachtelt) */ } 10 Node *root, *lmost; long size; key K size 5 public: kleinstes Element im Baum 8 20 left right typedef Node* link; typedef long size_type; 15 30 typedef K key_type; typedef K value_type; Anforderungen an die Element-Klasse K Tree() : root(0), lmost(0), size(0) { } •Standard-Konstruktor ~Tree() { clear(); } •Copy-Konstruktor void clear(); class _Iter { •bool K::operator<(const K&) const link p; •ostream& operator<<(ostream&,const K&); public: friend class Tree; _Iter( link ptr=0 ) : p(ptr) { } K& operator* () const { return p->key; } bool operator==( const _Iter it ) const { return p == it.p; } bool operator!=( const _Iter it ) const { return p != it.p; } class Tree implementiert ein Wörterbuch(dictionary) _Iter& operator++(); }; // END class _Iter typedef _Iter iterator; Basisoperationen im Baum friend class _Iter; • Suchen _Iter find( const K& t ) const { return findR(t, root ); } std::pair<_Iter,bool> insert(const K& t){return insertR(t,root);} • Einfügen • Entfernen size_type erase( const K& t ) { return eraseR(t, root ); } (=Wörterbuchoperationen) 14 Nichtlineare Datenstruktur Baum /*19.01.2011 Deklaration der Hilfsfunktionen findR, insertR, eraseR*/ }; Suchen im BST FB Informatik Prof. Dr. R.Nitsch _Iter Tree::find(const Key& key) { return findR(t,root);} Rekursiver Algorithmus für Suche im Baum _Iter Tree::findR( const Key& key, link r ) const Wenn der Teilbaum leer ist, liegt ein { if( r==0 ) return end();// Base case 1: Suchfehler Suchfehler vor. Wenn der Suchschlüssel t kleiner als der Wurzel-Schlüssel ist, suche im linken Teilbaum größer als der Wurzel-Schlüssel ist, suche im rechten Teilbaum den Suchschlüssel enthält, liegt ein Suchtreffer vor. if( key<r->key ) return findR( key, r->left ); else if( r->key<key ) return findR( key, r->right ); else // Base case 2: r->key == key ; Suchtreffer! return _Iter(r); } Wichtig: Algorithmus benötigt ausschliesslich K::operator<. um Gleichheit (Äquivalenz) festzustellen. Dabei vergleicht operator< nur die Schlüsselwerte der Knotenelemente. 19.01.2011 Nichtlineare Datenstruktur Baum 10 8 20 15 30 15 Einfügen in einen BST 50, 40, 43 Die Operation "Einfügen" hängt neue Knoten immer als Blatt an. (a) linker Teilbaum hat nur kleinere Schlüsselwerte als die Wurzel (b) rechter Teilbaum hat nur größere oder gleiche Schlüsselwerte als die Wurzel 2 Schritte: 10, 8, 20, 10, 30 50 10 8 40 43 (a) FB Informatik Prof. Dr. R.Nitsch 20 10 30 Duplikat! (b) Suchen der Einfügestelle Tree::iterator Tree::insert( const K& k ) { return insertR( k, root ); } Einketten (außer Duplikate) pair<Tree::iterator,bool> Tree::insertR(const Key& key,link& r) { // Füge Objekt k in subtree mit if( r==NULL ) { // Wenn der Subtree leer ist, wird t als Wurzelknoten eingefügt // Wurzel r ein (rekursiv) r = new Node(key); // Fügt eine Kopie des Objekts t ein Hier ist wichtig, dass der link als Referenz ++size; übergeben wurde: link& == Node*& if( lmost == NULL ) lmost = r; // Prüfen, ob neuer kleinster parent else if( r->key < lmost->key ) lmost = r; // Wert vorliegt return pair<_Iter,bool>(_Iter(r),true); key Node*& } // Gibt Iterator auf eingefügten Knoten zurück left right pair<_Iter,bool> success; Node* if( key < r->key ) // Objekt in linken subtree einfügen, wenn Schlüssel < Wurzel { success = insertR( key, r->left ); r->left->parent = r; return success; } // Objekt in rechten subtree einfügen, wenn Schlüssel ≥ Wurzel parent else if( r->key < key ) key { success = insertR( key, r->right ); r->right->parent = r; left right return success; } else return pair<_Iter,bool> (_Iter(r),false); } 19.01.2011 16 Nichtlineare Datenstruktur Baum Traversieren eines Binärbaumes FB Informatik Prof. Dr. R.Nitsch Problem: Man möchte alle Daten, die in einem Binärbaum gespeichert sind, besuchen zum Zwecke der Verarbeitung der assoziierten Daten; z.B. als Tabelle ausgeben oder ihre Anzahl oder ihre Summe berechnen Mit "Traversieren" eines Binärbaumes wird das systematische Besuchen all seiner Knoten bezeichnet Enthält ein Baum N Knoten, so gibt es N! (Fakultät) verschiedene Reihenfolgen, in welchen man die Knoten eines Binärbaumes besuchen kann Beispiel: Ein Baum mit den 3 Knoten A, B, C kann in 3! = 6 verschiedenen Reihenfolgen traversiert werden ABC BAC CAB ACB BCA CBA Die drei wichtigsten Traversierungsvarianten eines Baums werden mit Preorder, Inorder- und Postorder-Reihenfolge bezeichnet und lassen sich rekursiv definieren: Preorder: Inorder: Postorder: 19.01.2011 Besuch der Wurzel, danach den linken und danach den rechten Teilbaum. Dort wiederholt sich die Besuchsreihenfolge Besuch des linken Teilbaums in Inorder-Reihenfolge, danach Besuch der Wurzel, danach Besuch des rechten Teilbaums in Inorder-Reihenfolge Besuch des linken Teilbaums in Postorder-Reihenfolge, danach Besuch des rechten Teilbaums in Postorder-Reihenfolge, danach Besuch der Wurzel Nichtlineare Datenstruktur Baum 17 Traversieren von BST 15.1.10 void Tree::toStreamRin( ostream& os, const link subtree ) const { // visits all keys in sorted order (recursive inorder traversal) if( subtree==NULL ) ; // nothing to do else { // print left subtree toStreamRin( os, subtree->left ); // print key os << subtree->key << ' '; // ostream& operator<<(ostream& os,const K&) // erforderlich // print right subtree toStreamRin( os, subtree->right ); } } FB Informatik Prof. Dr. R.Nitsch g 4 c a 1 2 k f j 3 5 6 m 7 in-order Reihenfolge g 1 void Tree::toStreamRpre( ostream& os, const link subtree ) const { c k // visits all keys in pre-order sequence 2 5 if( subtree==NULL ) ; // nothing to do a f j m else { 3 4 6 7 // print key os << subtree->key << ' '; pre-order Reihenfolge // print left subtree toStreamRpre( os, subtree->left ); Anwendungen // print right subtree in-order Traversieren besucht Elem. sortiert toStreamRpre( os, subtree->right ); } pre-order Traversieren wird z.B. im Copy} Konstruktor und operator= verwendet (erzeugt exakte Baum-Kopie) 19.01.2011 Nichtlineare Datenstruktur Baum 18 Traversieren von BST FB Informatik Prof. Dr. R.Nitsch void Tree::toStreamRpost( ostream& os, const link subtree ) const { //visits all keys in post-order sequence recursive if( subtree == NULL ) ; // nothing to do else { // print left subtree toStreamRpost( os, subtree->left ); // print right subtree toStreamRpost( os, subtree->right ); // print key Anwendungen os << subtree->key << ' '; Post-order Reihenfolge wird } return; z.B.beim Löschen eines Baums } angewendet 19.01.2011 Nichtlineare Datenstruktur Baum g 7 c a 1 3 k f j 2 4 6 m 5 post-order Reihenfolge 19 Klasse Tree: Copy-Konstruktor und Zuweisung Tree( const Tree& toCopy ) { root = NULL; lmost=NULL; size=0; copyR( toCopy.root ); } Copy-Konstruktor Leeren Baum erzeugen Source-Tree kopieren FB Informatik Prof. Dr. R.Nitsch g c 2 1 k 5 a f j m void Tree::copyR( link subtree ) { 3 4 6 7 // Rekursiver Algorithmus (Traversieren in preorder Reihenfolge) // Erstellt eine exakte Kopie des Teilbaums pre-order Reihenfolge if( subtree == NULL ) ; // Nichts mehr zu kopieren else { this->insert( subtree->key ); // Kopie der Wurzel in Zielbaum (this) einfügen this->copyR ( subtree->left ); // linken Teilbaum kopieren this->copyR ( subtree->right ); // rechten Teilbaum kopieren } } Anwendung im Zuweisungsoperator Pre-order Reihenfolge Tree& Tree::operator=( const Tree& r_value ) { if( this == &r_value ) return *this; // self assignment this->clear(); Bisherigen Inhalt des L-value löschen this->copyR( r_value.root ); Source-Tree kopieren } 19.01.2011 Nichtlineare Datenstruktur Baum 20 Baum fällen in C++ FB Informatik Prof. Dr. R.Nitsch void Tree::clear() { clearR( root ); lmost = 0; } void Tree::clearR( link& subtree ) { // deletes subtree with root subtree using post-order traversal if( subtree!=NULL ) { clearR( subtree->left ); //erase left subtree post-order clearR( subtree->right ); //erase right subtree Reihenfolge delete subtree ; //erase root subtree = NULL; //and replace with external node --size; } return; } g 7 c a 1 3 k f j 2 4 6 m 5 post-order Reihenfolge Anwendung im Destruktor ~Tree() { clear(); } 19.01.2011 Nichtlineare Datenstruktur Baum 21 Rekursive Berechnung von Baummetriken FB Informatik Prof. Dr. R.Nitsch Rekursive Ermittlung der Knotenanzahl (Alternative zur Zählung mittels speziellem Attribut): private: long Tree::countR( link subtree ) { // zähle Knoten im Teilbaum if ( subtree==NULL ) return 0; // Base case: "Da ist kein weiterer Knoten mehr!" return countR( subtree->left ) // Zähle Knoten im linken Teilbaum + countR( subtree->right ) // Zähle Knoten im rechten Teilbaum und addiere hinzu + 1; // Das ist für den Wurzelknoten root } 10 public: long Tree::count() { return countR( root ); } // zähle Knoten im gesamten Baum 8 Rekursive Ermittlung der (Teil-)Baum-Höhe: 20 10 30 private: long heightR( link subtree ) { if ( subtree==NULL ) return 0; Externer Knoten: Höhe=0 -> base case long hl = heightR( subtree->left ); Bestimme Höhe des rechten Teilbaums long hr = heightR( subtree->right ); Bestimme Höhe des linken Teilbaums if( hl>hr ) return hl+1; else return hr+1; Wurzelknoten liegt 1 Ebene höher als seine Teilbäume! } public: long getHeight() { return heightR( root ); } 19.01.2011 Nichtlineare Datenstruktur Baum 22 Ausgabe der Baumstruktur als Textsequenz FB Informatik Prof. Dr. R.Nitsch Anwendung: Unterstützung beim Debuggen von Algorithmen mit Bäumen private: void Tree::toStreamR( ostream& os, link r, int indent ) { if( r==NULL ) { os << string(indent , ' ' ) << '*' << endl; // Externer Knoten -> base case return; } toStreamR( os, r->right, indent+1 ); os << string(indent, ' ' ) << r->key << endl; inorder traversieren toStream( os, r->left, indent+1 ); } public: void toStream( ostream& os ) { toStreamR( os, root, 0 ); } Beispiele: Einfügesequenz: 1, 2, 3, 4 Ausgabe: * 4 * 3 indent Ausgabe: * 4 * 3 * 2 * 1 * 19.01.2011 Einfügesequenz: 3, 1, 2, 4 * 2 * 1 * Hausaufgabe: Modifizieren Sie den Quellcode für den Fall, dass die Feldbreite für info w Zeichen (w>1) beträgt. Nichtlineare Datenstruktur Baum 23 Löschen von einzelnen Elementen im Baum Fallunterscheidung 1. Zu löschendes Element D ist ein Blatt: Entferne Blatt FB Informatik Prof. Dr. R.Nitsch vorher nachher D vorher nachher 2. Zu löschendes Element D hat genau einen Nachfolger N: Ersetze D durch N N D N 3. Zu löschendes Element D hat 2 Nachfolger DL und DR: Methode 2: Ersetze Schlüsselwert im Element D Methode 1: Ersetze D durch den linken durch eine Kopie des größten Schlüsselwerts im Nachfolger DL und mache den rechten linken Teilbaum von D (Element R) und lösche R. Nachfolger DR zum rechten Nachfolger des vorher nachher größten Elements R des linken Teilbaums von D vorher nachher D R D DL DR DL R 19.01.2011 DR DL R R DR RL Nichtlineare Datenstruktur Baum DL DR RL Vorteil: Geringere Auswirkung auf die Ausgeglichenheit 24 Löschen von einzelnen Elementen im Baum FB Informatik Prof. Dr. R.Nitsch // Schlüsselwert k suchen und entfernen Tree::size_type Tree::erase( const Key& key ) { // Nichts zu tun; nichts gelöscht if( empty() ) return 0; size_type numberOfErasedNodes = eraseR( key, root ); return numberOfErasedNodes; // nur 0 und 1 sind mögliche Werte (keine Duplikate im Tree!) } // class Tree erlaubt keine Duplikate Tree::size_type Tree::eraseR( const Key& key, link& subtree ) { // Base case 1: (Teil)Baum leer; nichts zu löschent // search key if( subtree==NULL ) return 0; // key k aus linkem Subtree entfernen if( key < subtree->key ) // Beachte: Ein Suchwert ist gefunden, wenn return eraseR( key, subtree->left ); // key k aus rechtem Subtree entfernen else if( subtree->key < key ) return eraseR( key, subtree->right ); // !(searched<current)&& !(current<searched) // d.h. Aquivalenzoperator (key_type::operator==) // wird nicht benutzt und muss daher auch nicht // definiert sein! // Base case 2: Key an Wurzel des Subtree gefunden. else // subtree->key == key // Zeiger auf gefundenes Lösch-Element zur besseren Lesbarkeit des Codes { link d = subtree; // Fall 1: Zu löschendes Element ist ein Blatt (keine Nachfolger) if( d->left==NULL && d->right==NULL ) vorher // Schlüssel ausketten { subtree = NULL; // Schlüssel löschen delete d; --size; return 1; } 19.01.2011 nachher D Nichtlineare Datenstruktur Baum 25 Löschen von einzelnen Elementen im Baum FB Informatik Prof. Dr. R.Nitsch if( d->left==NULL) { vorher nachher // Fall 2: Nur rechter Nachfolger subtree = d->right; // Schlüssel ausketten sroot D subtree->parent = d->parent; delete d; --size; return 1; // Schlüssel löschen } nachher vorher else if( d->right==NULL) { // Fall 2: Nur linker Nachfolger subtree = d->left; sroot // Schlüssel ausketten D subtree->parent = d->parent; // Schlüssel löschen delete d; --size; return 1; } else // d->left!=NULL && d->right!=NULL // Fall 3: 2 Nachfolger return erase2child( d->left, d ); // Maximum aus linkem Subtree // ins ple-Element kopieren und löschen } //else subtree->key == k } size_type erase2child( link& subtree, link& d ) { if( subtree->right!=NULL ) // Solange rechts weitersuchen, bis Maximum gefunden return erase2child( subtree->right, d ); vorher else { // subtree->right != 0; Maximum r gefunden ple link r = subtree; // Verbessert Lesbarkeit l d->key = r->key; // r nach d kopieren sroot if(r->left!=NULL) // wenn r kein Blatt… r->left->parent = r->parent; // … rl an Stelle von r s LR LL // einketten subtree = r->left; r delete r; --size; return 1; // r löschen rl } } 19.01.2011 Nichtlineare Datenstruktur Baum nachher r LL s LR rl 26 Der Tanz der Bäume - Rotationen und ihre Anwendungen Eine Rotation vertauscht die Rolle vorher B einer Wurzel (S bzw. B) mit einem ihrer unmittelbaren Nachfolger (E). Eine Rechts-(Links-)Rotation wirkt auf die Wurzel und den linken (rechten)rotierendes Element E Nachfolger Durch eine Rechts-Rotation wird das el rotierende Element und sein linker Teilbaum um eine Ebene angehoben während die Wurzel (S) und ihr rechter Teilbaum um eine Ebene vorher abgesenkt wird. Entsprechendes gilt für die LinksB Rotation. Anwendungen Einfügen an der Wurzel Löschen von Elementen in ausgeglichenen BST FB Informatik Prof. Dr. R.Nitsch Wurzel nachher B S E S sr el er er sr E≤ Schlüsselwerte <S nachher S S E E B er bl el er bl el Rechtsrotation (oben) und Linksrotation (unten) 19.01.2011 Nichtlineare Datenstruktur Baum 27 Einfügen an der Wurzel in einem BST A Standardimplementierung: Zuletzt eingefügte Schlüssel haben immer einen langen Suchpfad: Sie bilden ein Blatt im Baum. Wenn Anwendungen auf neu eingefügte Elemente öfter zugreifen als auf andere, sollte man besser an der Baumwurzel einfügen. Für die dabei auftretenden Probleme gibt es eine Lösung: Einfügen als Blatt und rekursive Anwendung von Rotationen: 1. 2. 3. 4. 5. G als Blatt einfügen Rechts-Rotation Links-Rotation Rechts-Rotation Links-Rotation 19.01.2011 X C E R 1 S C G R X 4 A G S E C A S X E G 2 R X C R G E 3 A Vorher E G 5 S G C S A A Probleme beim Einfügen an L der Wurzel FB Informatik Prof. Dr. R.Nitsch A R L C R Fehler! X R C Wenn man auch die Operation "Suchen" so ändert, daß jeder Suchtreffer durch rekursive Rotationen an die Wurzel befördert wird, erhält man ein selbstorganisierendes Suchverfahren, das häufig angesprochene Elemente an der Wurzel sammelt. Nachher Nichtlineare Datenstruktur Baum 28 22.1.2010 Die Grundschritte - Implementation der Rotationstransformationen void Tree::rotR( link& s ) { vorher b // Linker Subtree von s fehlt; nichts zu rotieren. if(e==NULL) return; // Linken Subtree mit Wurzel e von s abkoppeln link e = s->left; s if(e->right!=NULL) { // Wenn rechter Subtree von e existiert e // Rechter Subtree von e wird neuer linker Subtree von s s->left = e->right; sr // Rückwärtslink von er nach s e->right->parent = s; // Nichts anzubinden -> ext. Knoten } else s->left = NULL; el er e->right = s; // s als rechten Subtree an e anbinden e->parent = s->parent; // Rückwärtslink von e zu b s->parent = e; // Rückwärtslink von s zu e s = e; // e an b anbinden Rotation = Rollentausch } void Tree::rotL( link& b ) { link e = b->right; if(e==NULL) return; if(e->left!=NULL) { b->right = e->left; b->right->parent = b; } else b->right = NULL; e->left = b; e->parent = b->parent; b->parent = e; b = e; } 19.01.2011 FB Informatik Prof. Dr. R.Nitsch e s el er Nichtlineare Datenstruktur Baum vorher sr parent key K left right zwischen Wurzel und einem ihrer Nachfolger // Rechten Subtree mit Wurzel e von b abkoppeln // Rechter Teilbaum von s fehlt; nicht zu rotieren. // Wenn rechter Subtree von e existiert // Linker Subtree von e wird neuer rechter Subtree von b // Rückwärtslink von el nach b // Nichts anzubinden -> ext. Knoten // b als linken Subtree an e anbinden // Rückwärtslink von b nach s bl // Rückwärtslink von b nach e // e an s anbinden nachher b nachher s b s e e b er el er bl el 29 Algorithmus zum Einfügen an der Wurzel FB Informatik Prof. Dr. R.Nitsch Im vorigen Beispiel wurde die Einfügestelle für G am Ende des Suchpfades A-T-E-R gefunden. Entlang dieses Suchpfades wird das Einfügeelement durch Rotationen zur Wurzel gebracht. Bei jedem Rotationsvorgang ist die Richtung der Rotation des Vorgängerelements gegeben durch das Kindschaftsverhältnis zum Vorgängerknoten: Einfügeelement ist linkes Kind rechtes Kind -> Rechtsrotation des Vorgängerknotens -> Linksrotation des Vorgängerknotens Das Einfügeelement wird zur Wurzel, wenn jeder Knoten des Suchpfades einmal rotiert wurde. A void insertRootR( const Key& key, link& s ) { T // Einfügeposition erreicht; Ende des Suchpfades (base case) if( s==0 ) { E s = new Node( key ); // Einfügen als Blatt C R return; 1 G } if( key < s->key ) {// beim linken Nachfolger weiter suchen insertRootR( key, s->left ); rotR( s ); // danach Rechtsrotation des Knotens } // beim rechten Nachfolger weiter suchen // danach Linksrotation des Knotens else { * Baumstruktur * Baumstruktur insertRootR( key, s->right ); rotL( s ); 5 5 vor dem nach dem * } * Einfügen: Einfügen des 4 4 } Elements k=2: * * public: _Iter insertRoot( const Key& key ) { insertRootR( key, root ); return begin(); } 19.01.2011 Nichtlineare Datenstruktur Baum 3 3 * 1 X * 2 * * 1 * 30 Nocheinmal: Löschen von einzelnen Elementen im Baum (Methode 3) Bilde aus linkem und rechtem Teilbaum des Löschelements D einen Verbundbaum (Methode joinLR). Dieser wird an Stelle des Löschelements mit dessen Vorgänger verbunden. Suche dazu kleinsten Schlüsselwert M im rechten Teilbaum und mache diesen zur Wurzel des rechten Teilbaums (wird von min2root geleistet) und Mache den linken Teilbaum zum linken Nachfolger der neuen Wurzel M. vorher FB Informatik Prof. Dr. R.Nitsch nachher D DL DR M M DL DR' Implementierung public: size_type Tree::erase( const K& key ) { return eraseR( key, root ); } Rekursive Löschmethode eraseR sucht Schlüsselwert k im (Teil)Baum mit Wurzel r und entfernt dieses Element. private: size_type Tree::eraseR( const K& key, link& r ) { if(r==NULL) return 0; // base case: externer Knoten erreicht; Löschschlüssel nicht gefunden! if( key<r->key ) return eraseR( key, r->left ); // im linken Teilbaum weiter suchen else if( r->key<key ) return eraseR( key, r->right ); // im rechten Teilbaum weiter suchen else { // k bei r gefunden link d = r; // Verbindung zu Löschknoten in d temporär halten r = joinLR( d->left, d->right ); // joinLR verbindet rechten und linken Teilbaum und gibt Zeiger if(r!=NULL) r->parent = d->parent; // auf neue Wurzel zurück. Diese wird an den Vorgänger des delete d; --size; return 1; // Löschknotens angekoppelt } } 19.01.2011 Nichtlineare Datenstruktur Baum 31 Nocheinmal: Löschen von einzelnen Elementen im Baum (Methode 3) FB Informatik Prof. Dr. R.Nitsch link joinLR( link a, link b ){ // Teilbäume a und b verbinden; Zeiger auf Verbundbaum-Wurzel zurück geben if( b==NULL ) return a; // rechter Teilbaum ist leer; a ist schon Verbundbaum-Wurzel min2root( b ); // Minimum in rechtem Teilbaum suchen und zur Wurzel machen b->left = a; // linken Teilbaum an Wurzel b anhängen if( a!=NULL ) a->parent = b; // Wenn linker Teilbaum nicht leer ist return b; // Neue Verbundbaum-Wurzel zurückgeben } void min2root( link& r ) { if( r->left != NULL ) { min2root( r->left ); rotR( r ); } assert( r->left==NULL ); return; } // Macht kleinsten Schlüsselwert eines (Teil)Baums zur Wurzel // In die linken Teilbäume hinabsteigen bis Blatt (=Nachfolger des Löschelements) // erreicht ist. Danach dieses Blatt mittels Rechtsrotationen zur Wurzel machen // Nachbedingung: Der linke Teilbaum zur Wurzel muss leer sein vorher nachher D DL 19.01.2011 Nichtlineare Datenstruktur Baum DR M M DL DR' 32 Bewertung von BST FB Informatik Prof. Dr. R.Nitsch Vorteile Einfüge- und Löschoperationen von O(1) Suchoperationen bestenfalls von O(log(N)) Nachteile: Hoher Speicheraufwand für die Verbindungen Die fehlende Ausgeglichenheit von BST läßt keine Leistungsgarantien zu: o Bereits sortierte (auch umgekehrt sortierte) Dateien, o Dateien mit vielen mehrfachen Schlüsseln, o Dateien mit abwechselnd großen und kleinen Schlüsseln können zu entarteten BST und damit quadratischen Konstruktionszeiten O(N2) und linearen Suchzeiten O(N) führen Abhilfe: Algorithmen, die einen BST explizit ganz neu ausgleichen (z.B. nach fester Anzahl von Einfügen- und Löschoperationen. 19.01.2011 Nichtlineare Datenstruktur Baum 33 Algorithmus zum Ausgleichen eines BST FB Informatik Prof. Dr. R.Nitsch Idee: 1 Median im BST suchen und an die Wurzel bringen: Linker und rechter Teilbaum enthalten N/2 bzw. N/2-1 Knoten. Gleiches mit den Teilbäumen rekursiv wiederholen bis Teilbäume leer sind. balanceR benötigt wird dazu: partR: Ein Algorithmus, der das k-kleinste Element auswählt und danach an die Wurzel bringt; für k=N/2 ist dies der Median Vorher 2 3 4 4 2 1 5 6 3 5 6 7 7 Nachher void Tree::balanceR( link& subtree ) // base case: externer Knoten oder Blatt (Nichts zu partitionieren) { if(subtree==NULL || countR(subtree)==1) return; partR(subtree, countR(subtree )/2 ); // Median bestimmen und zur Wurzel rotieren balanceR( subtree->left ); // Gleiches für linken und rechten Teilbaum wiederholen balanceR( subtree->right ); } void partR( link& subtree, int k ) { int n = countR(subtree->left); // n: Knotenanzahl im linken Teilbaum if(k<n) { // k-kleinstes Element muß im linken Teilbaum sein partR(subtree->left,k); rotR(subtree); } else if(n<k) { // k-kleinstes Element muß im rechten Teilbaum sein. partR(subtree->right,k-n-1); rotL(subtree); // Dort ist es aber das (k-n-1)-kleinste! } else ; return; 34 Nichtlineare Datenstruktur Baum } 19.01.2011 Rot-Schwarz-Bäume FB Informatik Prof. Dr. R.Nitsch Das gelegentliche vollständige Ausgleichen (z.B nach einer bestimmten Anzahl von Einfüge/Lösch-Operationen) verbessert die Leistungsfähigkeit eines BST nur begrenzt. Insbesondere können immer noch keine Leistungsgarantien gegeben werden, weil die Höhe log(N) nicht immer garantiert ist. Sortierte höhen-balancierte Schlüsselbäume bieten hier mehr: Die Operationen Einfügen und Suchen sind in höhen-balancierten Bäumen am schnellsten. Allerdings steigt bei diesen der algorithmische Aufwand: Unzulässige Höhenunterschiede zweier Teilbäume müssen erkannt und mittels Rotationen beseitigt werden. Ein bekannter Vertreter dieser Spezies ist der Rot-Schwarz-Baum (Red-Black-Tree). Die assoziativen STL-Container gehören auch dazu. RBT verwalten ein zusätzliches Farbattribut pro Knoten RBT mit N internen Knoten garantieren: maximale Pfadlänge <= 2 * minimale Pfadlänge Baumhöhe <= 2 log (N+1) Suchen, Auswählen -> O(log N) garantiert 19.01.2011 Nichtlineare Datenstruktur Baum 35 Definition und weitere Eigenschaften FB Informatik Prof. Dr. R.Nitsch Ein Rot-Schwarz-Baum (RBT) ist ein sortierter binärer Schlüsselbaum mit folgenden Eigenschaften (Invarianten): 1. 2. 3. 4. Jeder Knoten ist entweder rot oder schwarz (RB1) Jeder externe Knoten ist schwarz (RB2) Rote Knoten haben immer 2 schwarze Söhne (RB3) Alle Pfade von externen Knoten zur Wurzel enthalten dieselbe Anzahl schwarzer Knoten (RB4) Beispiel für einen RBT K F C A S H D B O L X class Tree { enum Color { RED='r', BLACK='s'}; struct Node { Node(const K& k, Color c=BLACK) :key(k),left(0), right(0),parent(0), Black is nobly color(c) { } ~Node() { } U Node *left, *right, *parent; K key; Color color; E }; typedef Node* link; // Rest wie gehabt Externe Knoten sind alle schwarz und nicht dargestellt! }; 19.01.2011 Nichtlineare Datenstruktur Baum 36 Einfügen und Suchen in einem RBT FB Informatik Prof. Dr. R.Nitsch Algorithmische Konsequenzen: Das Suchen funktioniert wie beim BST-Tree -> Laufzeit O(h)=O(log N) Das Einfügen eines Knotens x erfolgt zunächst wie beim BST. Falls dabei Eigenschaft 3 oder 4 verloren geht, muss die Farbstruktur des Baumes repariert werden. RB1: Jeder Knoten ist entweder rot oder schwarz RB2: Jeder externe Knoten ist schwarz RB3: Rote Knoten haben immer 2 schwarze Söhne RB4: Alle Pfade von externen Knoten zur Wurzel enthalten dieselbe Anzahl schwarzer Knoten Einfügen eines Knotens x erfolgt zunächst immer als Blatt. Dabei wird x immer rot eingefärbt. Grund: weniger Farb korrekturen, weil - Bei schwarzem x würde immer RB4 verletzt! -Bei rotem x wird nur dann eine Regel (RB3) verletzt, 3 wenn der Vater von x auch rot ist (s. Beispiel). Farben reparieren 6 o g 6 12 Wenn das rote x auch einen roten Vater v hat, x unterscheidet man: 8 Fall 1: Onkel o von x (3) ist auch rot -> o und v schwarz machen Problem: Jeder Pfad über den Großvater g hat jetzt Neu eingefügt! einen zusätzlichen schwarzen Knoten. Lösung: g rot machen Konsequenz: RB3&4 können jetzt 2 Etagen höher wieder verletzt sein! 19.01.2011 Nichtlineare Datenstruktur Baum v 3 g o 12 8 x 37 v Einfügen in einem RBT - Implementierung FB Informatik Prof. Dr. R.Nitsch pair<Tree::_Iter,bool> Tree::insert_rb( const Key& key ) { if( root!=NULL ) assert( root->color == BLACK ); pair<_Iter,bool> success = insertR( key, root ); // insertR wird wiederverwendet if (success.second==false) return success; // Duplikat! Nichts eingefügt -> Korrekturen überflüssig! // Akteure für Korrekturen definieren link x = success.first.nodePtr; // Eingefügter ( standardmäßig scharzer) Knoten link v = x->parent; // Vater von x if( v==NULL ) return success; // x ist Baumwurzel; fertig link g = v->parent; // Grossvater von x link o = NULL; // Onkel von x; noch unbekannt Color ocolor; // Farbe des Onkels x->color = RED; // Eingefügt wird immer zunächst als roter Knoten if( g==NULL ) return success; // Vater ist Wurzelknoten und daher schwarz; x ist jetzt rot -> alles ok! // Vater und Großvater existieren 6 3 g o 12 8 19.01.2011 Nichtlineare Datenstruktur Baum v x Neu eingefügt! 38 Einfügen in RBT - Implementierung FB Informatik Prof. Dr. R.Nitsch // Vater und Großvater existieren // RBT-Invarianten müssen geprüft werden if( v->color==RED ) // Roter Vater (v) mit rotem Sohn (x) -> RB3 verletzt { while( x!=root && v->color==RED //) Auf dem Weg zur Wurzel dürfen keine 2 roten Knoten { // Akteure neu bestimmen // aufeinander folgen (RB3) g = v->parent; // g ist Großvater von x if( v==g->left ) // Wenn Vater linker Sohn des Großvaters ist { // dann ist Onkel von x der rechte Sohn des Großvaters o = g->right; g 6 if(o==NULL) ocolor = BLACK; // Ein nil-Onkel ist immer schwarz->Fall 2 else ocolor = o->color; if( ocolor==RED ) // Fall 1: Roter Vater und roter Onkel { o 3 12 v // Vater umfärben v->color = BLACK; // Onkel umfärben o->color = BLACK; // Großvater umfärben g->color = RED; x 8 // Hier ist jetzt alles erledigt // aber 2 Stufen höher jetzt alles nochmal // Deshalb Akteure neu bestimmen x = g; // Fortsetzen als wäre soeben roter g eingefügt worden v = x->parent; // v ist Vater von x Neu eingefügt! } else //ocolor is BLACK // Fall2: Roter Vater und schwarzer Onkel 19.01.2011 Nichtlineare Datenstruktur Baum 39 Einfügen in einem RBT FB Informatik Prof. Dr. R.Nitsch RB1: Jeder Knoten ist entweder rot oder schwarz (RB1) RB2: Jeder externe Knoten ist schwarz (RB2) RB3: Rote Knoten haben immer 2 schwarze Söhne (RB3) RB4: Alle Pfade von externen Knoten zur Wurzel enthalten dieselbe Anzahl schwarzer Knoten Fall 2: Onkel o von x ist schwarz oder nil -> Farben reparieren durch rotieren! Hinweis: nil steht für "not in list" und steht für einen nicht existierenden (=externen) Knoten. Fall 2a: x ist linker Sohn seines Vaters v. 1. Vater v (5) schwarz färben -> RB4 verletzt 2. Großvater g wieder rot färben -> RB4 verletzt 3. R-Rotation um g (9) -> fertig, alles ok! 1. 9 5 2 v x 2. g 9 o 5 2 x v g 9 o 5 2 v 3. g o 5 2 9 v x Neu eingefügt! 19.01.2011 Nichtlineare Datenstruktur Baum 40 Einfügen in einem RBT FB Informatik Prof. Dr. R.Nitsch RB1: Jeder Knoten ist entweder rot oder schwarz (RB1) RB2: Jeder externe Knoten ist schwarz (RB2) RB3: Rote Knoten haben immer 2 schwarze Söhne (RB3) RB4: Alle Pfade von externen Knoten zur Wurzel enthalten dieselbe Anzahl schwarzer Knoten Fall 2: Onkel o von x ist schwarz oder nil -> Farben reparieren durch rotieren! Hinweis: nil steht für "not in list" und steht für einen nicht existierenden (=externen) Knoten. Fall 2b: x ist rechter Sohn seines Vaters v. 1. L-Rotation um v (5) -> Fall 2a 2. Rest wie Fall 2a Fall 2a Fall 2a, 1. bis 3. 1. 9 5 v g 9 g 7 o v x 5 7 9 g v 7 x 5 Rollentausch! x Neu eingefügt! 19.01.2011 Nichtlineare Datenstruktur Baum 41 Einfügen in RBT - Implementierung { if( x==v->right ) { rotL(g->left); x = v; v = x->parent; } v->color = BLACK; g->color = RED; FB Informatik Prof. Dr. R.Nitsch // Fall 2: Roter Vater und schwarzer Onkel // Fall 2b: x ist rechter Sohn // erst in Fall 2a umwandeln // Rollentausch zwischen v und x // Ab hier Fall 2a // Vater umfärben // Großvater auch // Farben durch Rotation um g reparieren // Von wem stammt der Großvater ab: Urgroßvater oder Gott/Allah (root) ? // Großvater hat einen Urgroßvater dessen linker oder rechter Sohn er ist if( g->parent ) g->parent->left==g ? rotR( g->parent->left ) : rotR( g->parent->right ); else // Großvater ist Adam und hat keinen Vorgänger ausser root rotR( root ); } // END ELSE ocolor is BLACK } else { ... } } // END if( v==g->left ) Vater ist linker Sohn des Großvaters // Vater ist rechter Sohn des Großvaters // Das ganze nochmal, aber rechts und links vertauschen, auch bei den Rotationen // END while Schleife terminiert } else ; //END IF(v->color==RED) // Schwarzer Vater mit rotem Sohn. Keine RBT-Regel verletzt root->color = BLACK; Die Wurzel ist immer schwarz Black is nobly return success; } 19.01.2011 Nichtlineare Datenstruktur Baum 42 Algorithmus zum Testen der RBT-Eigenschaften FB Informatik Prof. Dr. R.Nitsch bool isRBTree() { isRBT = true; blackHeight = -1; // Anzahl schwarzer Knoten auf dem Weg von der Wurzel zu einem Blatt (Attribut von Tree) if (root!=NULL) return isRBTreeR( root, 0 ); else return true ; } bool isRBTreeR( link r, int bheight ) { // Schwarzhöhe für diese Aufrufebene aktualisieren if ( r->color==BLACK ) ++bheight; if(r->left==NULL && r->right==NULL) { // Blatt erreicht? (base case) if( blackHeight==-1 ) { // Wenn dies erstes Blatt ist braucht RB4 noch nicht geprüft werden ... blackHeight = bheight; // aber der Referenzwert für die Schwarzhöhe muss gemerkt werden. return true; // Regel RB3: externe Knoten sind immer schwarz (RB2) } else // Alle weiteren Blätter müssen die gleiche return (blackHeight==bheight) ? true : false; // Schwarzhöhe haben } if( r->left ) isRBT = isRBT && isRBTreeR(r->left, bheight ); // Blätter im linken Teilb. suchen if( r->right ) isRBT = isRBT && isRBTreeR(r->right, bheight ); // Blätter im rechten Teilb. suchen if( r->color==RED ) { // RB3 prüfen: Rote Väter haben stets 2 schwarze Knaben if(r->left) isRBT = isRBT && ( r->left->color==BLACK ); if(r->right) isRBT = isRBT && ( r->right->color==BLACK ); } return isRBT; } 19.01.2011 Nichtlineare Datenstruktur Baum 43 Algorithmus zum Testen des Einfügealgorithmus FB Informatik Prof. Dr. R.Nitsch void testInsert_RB() { Tree tree; for( int m=0; m<100; ++m ) { srand(m); for ( int i=0; i<10000; ++i ){ tree.insert_rb( rand() ); } assert( tree.isRBTree() ); tree.clear(); } } Aufgabe: Zeichnen Sie die RBT-Baumstruktur nach dem Einfügen folgender Zahlenfolgen: 5362 5364 531 534 19.01.2011 Nichtlineare Datenstruktur Baum 44 FB Informatik Prof. Dr. R.Nitsch So, das war´s! Was vergessen? 19.01.2011 Nichtlineare Datenstruktur Baum 45