next up previous contents index
Nächste Seite: Testen des Programms Aufwärts: Der Algorithmus Vorherige Seite: Einfuegen in einen Rot-Schwarz   Inhalt   Index


Loeschen aus einem Rot-Schwarz Baum

Der erste Teil des Algorithmus ist wieder aequivalent zu dem Loeschen aus einem binaeren Suchbaum. Zuerst lokalisieren wir den zu loeschenden Knoten wieder mit Hilfe der binaeren Suche. Wir nennen den zu loeschenden Knoten z.

/**********************************************************************

    * Datum: 11.12.1999

    * Diese Methode loescht ein Element. Die Methode ist public und muss

    * zum Loeschen benutzt werden. 

    * @return true wenn der Schluessel erfolgreich geloescht wurde.

    * @return false wenn der Schluessel nicht vorhanden ist.

    ***********************************************************************/    

    public boolean delete(int key) {

        rbelement x,y,z;

        

        z=root;

        

        while (z!=NIL) {                        // Standard binaere Suche

            if (key==z.searchkey) 

                break;                          // z ist das Element, das wir

                                                // loeschen.

            else

                z=(key<z.searchkey)?z.left:z.right;

        }

        

        if (z==NIL) return false;               // Den Schluessel gibt es nicht

                                                // in unserem Baum.

Die Besonderheit an dieser binaeren Suche ist, dass wir, sobald wir das Element gefunden haben, die Schleife unterbrechen mit der break - Anweisung. Wird der gesuchte Schluesselwert nicht gefunden, so bricht die Schleife am Ende des Baumes, wenn der zu durchsuchende Baum leer (NIL) ist, ab. z hat in diesem Fall den Wert NIL, und der Algorithmus terminiert nicht erfolgreich, es wird der Wert ``false'' zurueckgeliefert. Falls z nicht NIL ist, so enthaelt die Variable z eine Referenz auf den zu loeschenden Knoten.

 

        if ((z.left==NIL) || (z.right==NIL)) {

            y=z;                                

        } else {

            y=z.right;

            while (y.left!=NIL) y=y.left;       // Nachfolger von z

        }

Damit gewaehrleistet ist, dass nach dem Loeschvorgang immer noch alle Knoten des Baumes erreichbar sind, koennen wir nicht ein beliebiges Element aus dem binaeren Baum entfernen. weil dann mindestens ein Unterbaum des geloeschten Knoten nicht erreichbar waere. Einen Knoten, der mindestens einen leeren Unterbaum besitzt, koennen wir hingegen loeschen, indem wir den Knoten durch seinen nicht-leeren Unterbaum oder, in dem Fall, dass der Knoten zwei leere Unterbaeume besitzt, durch einen leeren Baum, ersetzen. Wenn der linke oder der rechte Unterbaum von z der leere Baum ist, so wird er als zu loeschender Knoten gemerkt (y:=z).

Hat z keine zwei NIL-Unterbaeume, so lokalisieren wir das zu z naechst groessere Element in dem Rot-Schwarz Suchbaum. Dieses befindet sich im rechten Unterbaum von z, aufgrund der binaeren Suchbedingung. Es handelt sich jedoch um das kleinste Element in dem rechten Unterbaum von z. Der Nachfolger y von z hat einen leeren linken Unterbaum, da es sonst aufgrund der binaeren Suchbedingung einen Knoten mit einem Suchschluessel kleiner als der von y und groesser als der von z gaebe. Das waere ein Widerspruch zu der Forderung, dass y den zu z naechst groesseren Suchschluessel in dem Baum besitzt.

Das Ziel ist, den zu loeschenden Knoten durch seinen Nachfolger zu ersetzen. Damit waere die binaere Suchbedingung gewahrt und der Baum waere wieder ein Bianerbaum.

An dieser Stelle des Algorithmus enthaelt y eine Referenz auf einen Knoten mit mindestens einem leeren Unterbaum. Dieser Knoten ist ausserdem entweder z oder der Nachfolger von z.

        if (y.left!=NIL)

            x=y.left;

        else

            x=y.right;

Ist y=z, und der linke Unterbaum von y ist nicht-leer, so wird der nicht-leere linke Unterbaum der Variablen x zugewiesen. Ist y der Nachfolger von z oder ist der linke Unterbaum von z der leere Baum, so wird der Variablen x eine Referenz auf den rechten Unterbaum, welcher durchaus auch NIL sein kann, zugewiesen.

        x.parent=y.parent;

        if (y.parent!=null)

            if (y==y.parent.left)

                y.parent.left=x;

            else

                y.parent.right=x;

        else

            root=x;

        if (y!=z)

            z.searchkey=y.searchkey;

An dieser Stelle wird y durch x ersetzt und alle Daten von y in z gespeichert. Das heisst, entweder haben wir z durch seinen Nachfolger y ersetzt, und den Nachfolger y auf triviale Weise, da er mindestens einen leeren Unterbaum besitzt, geloescht, oder wir haben z auf triviale Weise geloescht.

Das Loeschen von Elementen kann die definierenden Eigenschaften eines Rot-Schwarz Baumes verletzen. Betrachten wir vorerst die moeglichen Faelle, die bei dem Loeschen auftreten koennen. Es gibt symmetrische Faelle, die hier nicht gezeigt sind.

Abbildung: 3 Faelle beim Loeschen

\resizebox*{3cm}{10cm}{\includegraphics{/development/pics/bild15a.ps}} \resizebox*{3cm}{10cm}{\includegraphics{/development/pics/bild15b.ps}} \resizebox*{3cm}{10cm}{\includegraphics{/development/pics/bild15c.ps}}

Das Minus-Zeichen im Fall c) deutet an, dass wir bei diesem Loeschvorgang einen schwarzen Knoten zuwenig haben. In den Faellen a) und b) wird keine der vier Rot-Schwarz Baum Eigenschaften verletzt.

Im Fall b) muss eine Farbe geaendert werden. Falls y ``rot'' ist, sind wir an diesem Punkt bereits fertig mit Loeschen, da wir einen roten Knoten stets loeschen koennen, und die Rot-Schwarz Baum Eigenschaften dennoch erhalten bleiben. Falls y schwarz ist, muessen wir wieder den Rot-Schwarz Baum ausgleichen, weil damit auf einem Pfad ein schwarzer Knoten zu wenig vorhanden ist.

Der Knoten x sei ein schwarzer Knoten in einem Rot-Schwarz Suchbaum T, und x sei der einzige Knoten, der die definierenden Eigenschaften eines Rot-Schwarz Baums verletzt. Abgesehen von x ist T ein Rot-Schwarz Baum.

Ist x das linke Kind seines Vorgaengerknoten, so bezeichnen wir das rechte Kind des Vorgaengerknotens als ``Schwester'' von x. Ist x das rechte Kind seines Vorgaengerknoten, so bezeichnen wir das linke Kind des Vorgaengerknotens als ``Schwester'' von x.

Fall 1: x ist die Wurzel. T ist ein Rot-Schwarz Baum

Abbildung: Loeschen Fall 2

\resizebox*{0.8\textwidth}{5cm}{\includegraphics{/development/pics/deletecase1.ps}}

Fall 2: x ist nicht die Wurzel, und seine Schwester u ist rot. Sei y der Elternknoten von x und u. Diesen Fall transformieren wir in den Fall 3 oder 4.

                if (u.color==RED) {

                    u.color=BLACK;

                    x.parent.color=RED;

                    rotateLeft(x.parent);

                    u=x.parent.right;

                }

                                  Fall 2a)

a) x ist das linke Kind von y: Rotiere y nach links, faerbe y rot und w schwarz. Wiederhole den Algorithmus.

b) x ist das rechte Kind von y: Rotiere y nach rechts, faerbe u schwarz und y rot. Wiederhole den Algorithmus.

Fall 3: x ist nicht die Wurzel, seine Schwester u ist schwarz und x ist das linke Kind seines Elternknoten y. z sei das linke und v das rechte Kind von u.

Abbildung: Loeschen Fall 3.1

\resizebox*{4cm}{0.38\textheight}{\includegraphics{/development/pics/deletebasics1.ps}} \resizebox*{4cm}{0.38\textheight}{\includegraphics{/development/pics/deletebasics2.ps}}

Fall 3.1: v und z sind schwarz.

                if ((u.left.color==BLACK) && (u.right.color==BLACK)) {

                    u.color=RED;

                    x=x.parent;

                } 

Fall 3.1.1: y ist schwarz. Wir faerben u rot und setzen den Algorithmus mit x:=y fort.

Fall 3.1.2: y ist rot. Wir faerben u rot und y schwarz. T ist ein Rot-Schwarz Baum.

Fall 3.2: v ist schwarz und z ist rot. Indem wir z schwarz faerben und u rot, anschliessend u nach rechts rotieren, wandeln wir den Fall 3.2 in den Fall 3.3 um. Hier der Quellcode zu dem Fall 3.2:

                    if (u.right.color==BLACK) {

                        u.left.color=BLACK;

                        u.color=RED;

                        rotateRight(u);

                        u=x.parent.right;

                    }

Abbildung: Loeschen Fall 3.3

\resizebox*{10cm}{16cm}{\includegraphics{/development/pics/deletebasics3.ps}}

Fall 3.3: v ist rot. Wir muessen u nach links rotieren, v schwarz faerben, die Farbe von u gleich der Farbe von y setzen und y schwarz faerben. Dann ist T ein Rot-Schwarz Baum. Der Quellcode dazu:

                    u.color=x.parent.color;

                    x.parent.color=BLACK;

                    u.right.color=BLACK;

                    rotateLeft(x.parent);

Damit laesst sich nun auch der Fall 3.2 graphisch darstellen:

Abbildung: Loeschen Fall 3.2. Der graue Knoten deutet an, dass er entweder rot oder schwarz sein kann

\resizebox*{7cm}{0.4\textheight}{\includegraphics{/development/pics/deletebasics4.ps}}

Fall 4: x sei das rechte Kind von y. Dieser Fall ist symmetrisch zu dem Fall 3.

Aehnlich wie bei dem Einfuegen wurden auch damit alle moeglichen Faelle behandelt. Hier der gesamte Algorithmus zum Ausgleichen des Baumes nach dem Loeschen:

    private void deleteFixup(rbelement x) {

        while ((x!=root) && (x.color==BLACK)) {

            if (x==x.parent.left) {

                rbelement u=x.parent.right;

                if (u.color==RED) {

                    u.color=BLACK;

                    x.parent.color=RED;

                    rotateLeft(x.parent);

                    u=x.parent.right;

                }

                if ((u.left.color==BLACK) && (u.right.color==BLACK)) {

                    u.color=RED;

                    x=x.parent;

                } else {

                    if (u.right.color==BLACK) {

                        u.left.color=BLACK;

                        u.color=RED;

                        rotateRight(u);

                        u=x.parent.right;

                    }

                    u.color=x.parent.color;

                    x.parent.color=BLACK;

                    u.right.color=BLACK;

                    rotateLeft(x.parent);

                    x=root;

                }

            } else {

                rbelement u=x.parent.left;

                if (u.color==RED) {

                    u.color=BLACK;

                    x.parent.color=RED;

                    rotateRight(x.parent);

                    u=x.parent.left;

                }

                if ((u.right.color==BLACK) && (u.left.color==BLACK)) {

                    u.color=RED;

                    x=x.parent;

                } else {

                    if (u.left.color==BLACK) {

                        u.right.color=BLACK;

                        u.color=RED;

                        rotateLeft(u);

                        u=x.parent.left;

                    }

                    u.color=x.parent.color;

                    x.parent.color=BLACK;

                    u.left.color=BLACK;

                    rotateRight(x.parent);

                    x=root;

                }

            }

        }

        x.color=BLACK;

    } // End of function deleteFixup

Nachdem man einen schwarzen Knoten entfernt hat, enthaelt jeder Pfad, der diesen Knoten enthielt, einen Knoten zu wenig. Damit ist die Rot-Schwarz Baum Eigenschaft 2 durch jeden Nachfahren von dem geloeschten Knoten verletzt. Wir koennten das Problem korrigieren, indem wir annehmen, der neue Knoten, der an der Stelle des geloeschten ist, haette ein ``zusatzliches'' Schwarz. Damit waere die Eigenschaft 2 wieder hergestellt, aber die Eigenschaft 1 verletzt, da ein Knoten nur entweder rot oder schwarz sein kann, und nicht doppelt schwarz. Unser Algorithmus, implementiert in der Methode deleteFixup, dient nun dazu, die Eigenschaft 1 wieder herzustellen.

Das Ziel der while-Schleife ist, das doppelte ``Schwarz'' in dem Baum nach oben zu bewegen, bis die Variable x entweder eine Referenz auf einen roten Knoten enthaelt, den wir dann in der letzten Zeile der Methode schwarz faerben, x eine Referenz auf die Wurzel des Baumes enthaelt, in welchem Fall wir das doppelte Schwarz einfach entfernen koennen, oder passende Umformung durchgefuehrt werden koennen. Innerhalb der while-Schleife enthaelt x eine Referenz auf einen schwarzen Knoten, der das ``doppelte'' Schwarz besitzt. Da der Knoten x ``doppelt'' schwarz ist, kann der Knoten w (die Schwester von x) nicht NIL sein.

Die Kernidee hinter dem Algorithmus ist, dass in jedem Fall die Anzahl der schwarzen Knoten von und einschliesslich der Wurzel des gezeigten Teilbaumes zu jedem Unterbaum 1 bis 5 gleich bleibt. Dabei wird das doppelte ``Schwarz'' des Knoten x beruecksichtigt.

Im Fall 3.1 und dem symmetrischen Fall sind x, die Schwester von x sowie beide Kinder der Schwester schwarz. Es wird ein ``Schwarz'' von x und dessen Schwester weggenommen, und dem Elternknoten der beiden eins hinzugefuegt. War der Elternknoten rot, so sind wir an der Stelle fertig, ansonsten enthaelt der Elternknoten von x das doppelte Schwarz und der Algorithmus wird mit diesem Knoten wiederholt.

Im Fall 3.3 (welcher 3.2 einschliesst) und dem symmetrischen Fall wird der Baum rotiert und der Algorithmus terminiert anschliessend.

Es stellt sich nun die Frage, in welcher Zeit die Operation Einfuegen laeuft. Zum Auffinden des zu loeschenden Elementes benutzen wir die bianere Suche, die in einem Rot-Schwarz Baum mit n Elementen \( O(\log n) \) Zeit benoetigt. In den Faellen 2, 3.2 und 3.3 sowie den symmetrischen Faellen terminiert der Algorithmus nach einer konstanten Anzahl von Farbvertauschungen und hoechstens 3 Rotationen. Im Fall 3.1 wird das doppelte Schwarz mit jedem Schleifendurchlauf um einen Knoten nach oben in Richtung Wurzel des Baumes verschoben. Da die Hoehe des Baumes eine natuerliche Zahl ist, verringert sich das Problem mit jedem Schleifendurchlauf um ``1''. Der Fall 3.1 laeuft demnach in einer Zeit von \( O(h) \) und damit \( O(logn). \) Damit ist die gesamte Komplexitaet des Einfuegens \( O(\log n) \).

Am Ende der Loeschen-Methode muessen wir noch sicherstellen, dass die Wurzel schwarz ist, damit unsere Vereinfachung im Einfuegen-Algorithmus weiterhin moeglich und gueltig ist.


next up previous contents index
Nächste Seite: Testen des Programms Aufwärts: Der Algorithmus Vorherige Seite: Einfuegen in einen Rot-Schwarz   Inhalt   Index
root 2000-01-25