Westfälische Wilhelms Universität Münster Datenstrukturen und Algorithmen Prof. Dr. Clausing WS 2000/2001 Binäre Suchbäume Arne Hüls 5. Semester Rudolf-Harbig-Weg 59 / B 311 48149 Münster [email protected] Inhaltsverzeichnis I. Bäume 1. Bäume im Allgemeinen 3 2. Binäre Suchbäume 4 3. Verwendung binärer Suchbäume 4 II. Funktionale und objektorientierte Realisierung binärer Suchbäume 1. Funktionale Realisierung 5 2. Code zur funktionalen Implementierung 5 3. Objektorientierte Realisierung 8 4. Code zur objektorientierten Implementierung 8 III. Eigenschaften zufällig erzeugter binärer Suchbäume 1. Zufällige Erzeugung 11 2. Eigenschaften zufällig erzeugter binärer Suchbäume 11 IV. Balancierte binäre Suchbäume 1. Vorteile balancierter binärer Suchbäume 13 2. AVL-Bäume 13 3. Sicherstellen der AVL - Bedingung 13 4. Implementierung von AVL – Bäumen 15 5. Code zur Implementierung von AVL – Bäumen 15 V. Anwendungsbeispiel : Implementierung einer dünn besetzten Matrix Dünn besetzte Matrizen 17 VI. Anhang 1. Literaturverzeichnis 18 2. Code zur Implementierung dünn besetzter Matrizen 18 2 I. Bäume 1. Bäume im Allgemeinen Bei einem Baum handelt es sich um eine dynamische Datenstruktur ähnlich einer Liste. Anders als bei einer linearen Liste kann aber ein Element eines Baumes, Knoten genannt, nicht nur einen, sondern mehrere Nachfolger, Kindknoten genannt, haben. Ein Knoten besteht üblicherweise aus einem Schlüssel zur Identifikation, einem Datenobjekt und Referenzen auf seine Kindknoten. Als Wurzel des Baumes bezeichnet man seinen ersten Knoten, der als einziger Knoten des Baumes keinen Vorgänger, Elternknoten genannt, hat. Hat ein Knoten als Nachfolger nur den leeren Baum ( entsprechend einer leeren Liste ), so nennt man ihn Blatt. Alle anderen Knoten bezeichnet man als innere Knoten. Wurzel innerer Knoten Blätter Der leere Baum Abb. 1 : grafische Darstellung eines Baumes Als Höhe eines Baumes bezeichnet man den Abstand von der Wurzel bis zum weitest entfernten Blatt des Baumes. Der Baum in Abb. 1 hat also die Höhe 2. Als Ordnung eines Baumes bezeichnet man die maximale Anzahl von Kindknoten, die ein Knoten des Baumes haben kann. In Abb. 1 ist die Ordnung 2. Die Verbindung zwischen einem Knoten und der Wurzel wird als Pfad bezeichnet. 3 2. Binäre Suchbäume Bäume der Ordnung 2 nennt man Binärbäume. Sie bestehen aus Knoten, die jeweils 2 Nachfolger haben. Man spricht deshalb nicht mehr allgemein von Kindknoten, sondern von linkem und rechtem Sohn. Sind die Knoten eines solchen Binärbaumes durch ihre Schlüssel so geordnet, dass für jeden Knoten gilt : Alle Schlüssel des linken Teilbaumes < Schlüssel des Knotens < Alle Schlüssel des rechten Teilbaumes, so spricht man von einem geordneten Binärbaum oder binären Suchbaum. Mit linkem bzw. rechtem Teilbaum sind hier die Teile des Baumes gemeint, deren erster Knoten der linke bzw. rechte Sohn des betrachteten Knotens ist. 4 9 1 7 Abb. 2 : binärer Suchbaum 3. Verwendung binärer Suchbäume Da man mit binären Suchbäumen gut Daten abspeichern und sehr schnell innerhalb gespeicherter Daten suchen kann, eignen sie sich z.B. zur Verwaltung von Daten in Katalogen oder Bibliotheken. Da das Suchen in binären Suchbäumen, verglichen mit linearen Datenstrukturen, sehr schnell geht eignen sie sich auch sehr gut zum Sortieren von Daten. Weitere Anwendungsbereiche sind u.a. Entscheidungsbäume und Codebäume. 4 II. Funktionale und objektorientierte Realisierung binärer Suchbäume 1. Funktionale Realisierung Es gibt verschiedene Möglichkeiten einen Binärbaum zu implementieren. Betrachten wir einmal den Unterschied zwischen der objektorientierten und der funktionalen Implementierung. Implementiert man den Baum funktional, so konstruiert man sich einen Baum und speichert bei jeder Operation auf diesem eine veränderte Kopie des alten Baumes. t = new BinTree( t = insert(37,t) t = insert(23,t) t = insert(42,t) etc. ); ; ; ; Würde man dabei nicht jedes Mal den alten Baum überschreiben, sondern jeweils einen neuen Baum speichern, so könnte man die Evolution des Baumes genau nachvollziehen. Diese Art der Speicherung erzeugt persistente Binärbäume. t1 = t2 = t3 = t4 = etc. new BinTree( ); insert(37,t1); insert(23,t2) ; insert(42,t3) ; Man würde also beim Einfügen den jeweils einzufügenden Schlüssel und den Baum, in den eingefügt werden soll an die entsprechende Methode übergeben. Diese würde dann wie eine mathematische Funktion in der Art y = f(a,b) einen Wert berechnen und ihn der entsprechenden Variablen zuweisen. 2. Code zur funktionalen Implementierung Hier, wie im folgenden, wird zur Darstellung der Implementierungsweise ein Java – ähnlicher Pseudocode verwendet. Da es sich bei Java um eine objektorientierte Programmiersprache handelt, ist die Darstellung funktionaler Vorgehensweisen etwas umständlich. Um aber eine Vergleichbarkeit der Codebeispiele zu gewähr- 5 leisten und da es im folgenden auch keine funktionalen Codebeispiele mehr geben wird, wird der Einheitlichkeit halber auch hier dieser Pseudocode benutzt. Einfügen : // Der Methode insert wird der einzufügende Schlüssel und der Baum in den // eingefügt werden soll übergeben. public BinTree insert ( int x , BinTree oldTree ) { // Es wird eine Kopie des alten Baumes erstellt und unter neuem Namen // gespeichert BinTree newTree = oldTree; // Ist der alte Baum leer, so wird der Schlüssel direkt eingefügt. In dieser // Implementierung wird jeder Knoten als Baum betrachtet. if ( oldTree.isEmpty ) { newTree.key = x; newTree.empty = false; } // // // // // // Ist der Baum nicht leer, so wird der Einfügeschlüssel mit dem Schlüssel des betrachteten Baumes verglichen. Ist der Einfügeschlüssel kleiner, so wird insert rekursiv mit dem Einfügeschlüssel und dem linken Sohn aufgerufen und das Ergebnis als neuer linker Sohn gespeichert. Der neue rechte Sohn ist eine Kopie des alten rechten Sohnes. Ist der Einfügeschlüssel größer, geschieht das Ganze seitenverkehrt. else { if ( x < oldTree.key ) { newTree.left=insert(x,oldTree.left); newTree.right=oldTree.right; } if ( x > oldTree.key ) { newTree.right=insert(x,oldTree.right); newTree.left=oldTree.left; } } return newTree; } Das Entfernen aus einem Baum ist etwas aufwändiger, da hierbei teilweise Teilbäume umstrukturiert werden müssen, um den Baum geordnet zu lassen. 6 Entfernen : // Der Methode remove wird der zu entfernende Schlüssel und der Baum aus dem // entfernt werden soll übergeben. public BinTree remove ( int x, BinTree oldTree ) { // Es wird eine Kopie des alten Baumes erstellt und unter neuem Namen // gespeichert BinTree newTree = oldTree // // // // // Es wird der Einfügeschlüssel mit dem Schlüssel des betrachteten Baumes verglichen. Ist der Einfügeschlüssel kleiner, so wird remove rekursiv mit dem Einfügeschlüssel und dem linken Sohn aufgerufen und das Ergebnis als neuer linker Sohn gespeichert. Der neue rechte Sohn ist eine Kopie des alten rechten Sohnes. if ( x < oldTree.key ) { newTree.left = remove(x, oldTree.left) newTree.right = oldTree.right } // Ist der Einfügeschlüssel größer geschieht das Ganze seitenverkehrt. if ( x > oldTree.key ) { newTree.right = remove(x, oldTree.right) newTree.left = oldTree.left } // Ist der Einfügeschlüssel gleich dem betrachteten Schlüssel, so werden die // Söhne betrachtet. if ( x = = oldTree.key ) { // Sind beide Nachfolger leer, so wird der aktuelle Teilbaum, der ja // entsprechend nur aus dem zu löschenden Knoten besteht, gelöscht. if ( oldTree.left.isEmpty && oldTree.right.isEmpty ) { newTree.empty = true } // Ist nur einer der beiden Nachfolger leer, so wird der Knoten durch den // jeweils anderen Nachfolger ersetzt. if ( oldTree.left.isEmpty && oldTree.right.isNotEmpty ) { newTree = oldTree.right } if ( oldTree.left.isNotEmpty && oldTree.right.isEmpty ) { newTree = oldTree.left } // // // // Ist keiner der beiden Nachfolger leer, so wird der betrachtete Knoten durch den größten Knoten aus seinem linken Nachfolgebaum ersetzt. Der linke Nachfolger wird der alte linke Nachfolger ohne den betreffenden größten Knoten und der rechte Nachfolger wird der alte rechte Nachfolger. 7 if ( oldTree.left.isNotEmpty && oldTree.right.isNotEmpty ) { newTree.key = oldTree.left.findMaxKey newTree.left = removeMax ( oldTree.left ) newTree.right = oldTree.right } } } 3. Objektorientierte Realisierung Implementiert man den Baum objektorientiert, so konstruiert man sich einmal ein Objekt Baum und verändert dieses Objekt bei jeder Operation. t = new BinTree ( ); t.insert(37); t.insert(23); t.insert(42) ; etc. Beim Einfügen wird also kein alter Baum übergeben, sondern nur der Schlüssel. Die Methode arbeitet auf dem Objekt, auf dem sie aufgerufen wird. Dies entspricht eher der Vorstellung, die man von einem Baum hat, mit Teilbäumen als Objekten, die verändert werden. 4. Code zur objektorientierten Implementierung Einfügen : // Der Methode insert wird nur der einzufügende Schlüssel // übergeben. public void insert ( int x ) { // Falls das Objekt auf dem gerade gearbeitet wird der leere Baum ist, so wird // der Einfügeschlüssel direkt eingefügt. if ( isEmpty ) { key = x empty = false } // // // // Sonst wird der Einfügeschlüssel mit dem aktuellen Schlüssel verglichen und rekursiv im entsprechenden Teilbaum eingefügt. Auch hier wird wieder jeder Knoten als Baum betrachtet, da er ja Wurzel des entsprechenden Teilbaumes ist. 8 else { if ( x < key ) { left=insert(x) } if ( x > key ) { right=insert(x) } } } Entfernen : // Der Methode remove wird nur der zu löschende Schlüssel übergeben. Public void remove ( int x ) { // Ist der Schlüssel kleiner, als der des aktuellen Objektes wird remove auf // dem linken Teilbaum ausgeführt. if ( x < key ) { left.remove(x) } // Ist er größer, auf dem rechten Teilbaum. if ( x > key ) { right.remove(x) } // Ist er gleich, so werden wieder die Nachfolger betrachtet. if ( x = = key ) { // Sind beide Nachfolger leer, wird der Teilbaum gelöscht. Ist nur einer der // Nachfolger leer, wird der aktuelle Knoten durch den jeweils anderen // Nachfolger ersetzt. if ( left.isEmpty && right.isEmpty ) { empty = true } if ( left.isEmpty && right.isNotEmpty ) { this = right } if ( left.isNotEmpty && right.isEmpty ) { this = left } // Sind beide Nachfolger nicht leer, so wird der größte Schlüssel im linken // Teilbaum gesucht, entfernt und das aktuelle Objekt durch ihn ersetzt. if ( left.isNotEmpty && right.isNotEmpty ) { key = left.findMax left.removeMax } } } 9 Man sieht, dass der Vorteil der objektorientierten Implementierung in der geringeren Veränderung liegt. Bei funktionaler Implementierung muss der Baum jedes Mal umkopiert und neu gespeichert werden, bei objektorientierter Realisierung wird das Objekt selbst verändert, und muss daher weder kopiert noch neu gespeichert werden. 10 III. Eigenschaften zufällig erzeugter binärer Suchbäume 1. Zufällige Erzeugung Betrachtet man einen binären Suchbaum mit n Schlüsseln, so hängt die äußere Gestalt des Baumes ganz entscheidend von der Einfügereihenfolge der enthaltenen Schlüssel ab. Fügt man z.B. die Schlüssel (1,2,..,n) in geordneter Reihenfolge in einen leeren Baum ein, so erhält man eine lineare Liste wie in Abb. 3. Fügt man dieselben Schlüssel ungeordnet ein, so erhält man einen völlig anderen Baum. Besteht ein Baum aus Schlüsseln im Intervall [a,b], und ist jede Einfügereihenfolge der Schlüssel, also alle Permutationen des Intervalls, gleich wahrscheinlich, so spricht man von einem zufällig erzeugten binären Suchbaum. 1 12 3 13 1 41 14 21 15 Abb. 3 : entartet zu linearer Liste Abb. 4 : zufällig erzeugter binärer Suchbaum Bei diesen zufällig erzeugten Bäumen ist dann aber nicht jede äußere Gestalt gleich wahrscheinlich. So erhält man nur durch eine bestimmte, nämlich die geordnete, Einfügereihenfolge einen Baum in Form einer linearen Liste wie in Abb. 3 . Der Baum aus Abb. 4 hingegen kann durch verschiedene Reihenfolgen ( z.B. (3,1,2,4,5) ; (3,4,5,1,2) ; (3,1,4,2,5) etc.) erzeugt werden, er entsteht also mit höherer Wahrscheinlichkeit. 2. Eigenschaften zufällig erzeugter binärer Suchbäume Es zeigt sich also, dass bei zufälliger Erzeugung eines binären Suchbaumes mit hoher Wahrscheinlichkeit ein Baum entsteht, der recht gut höhenbalanciert ist. Dies heißt nicht, dass alle zufällig erzeugten Bäume balanciert sind, es entstehen nur mit höherer Wahrscheinlichkeit balancierte Bäume. 11 51 Zur mittleren Höhe eines zufälligen binären Suchbaumes lässt sich folgender Satz beweisen : Die mittlere Höhe eines zufälligen Binärbaumes mit n Schlüsseln erfüllt : hn ≤ 2(c + 1)H n + 2 = O(log n) 1 2 wobei : H n = 1 + + ... + 1 und (ln c −1) ⋅ c = 2 n Der genaue Wert wurde erst 1986 von L. Devroye berechnet. 12 IV. Balancierte binäre Suchbäume 1. Vorteile balancierter binärer Suchbäume Wie in III.1 gesehen können binäre Suchbäume im schlechtesten Fall zu linearen Listen entarten. Dies führt dazu, dass Operationen wie Suchen, Einfügen, Entfernen, etc. im schlechtesten Fall einen Aufwand der Größenordnung O(n) benötigen. Damit geht der große Vorteil der Baumstruktur gegenüber einer linearen Liste verloren. Um sicherzustellen, dass der Aufwand für diese Operationen auch im schlechtesten Fall nicht größer als O(log n) wird, muss der Baum balanciert werden. Es gibt verschiedene Möglichkeiten der Balancierung, wie z.B. Gewichtsoder Höhenbalancierung. Dazu muss der Baum nach jeder Operation feststellen, ob das Balance-Kriterium noch erfüllt ist und sich gegebenenfalls neu organisieren. 2. AVL – Bäume Das bekannteste Beispiel für balancierte binäre Suchbäume sind wohl die, 1962 nach Adelson-Velskii und Landis benannten, AVL – Bäume. Es handelt sich hierbei um höhenbalancierte Bäume, die neben den normalen Bedingungen für binäre Suchbäume noch eine Bedingung betreffend ihrer Höhe erfüllen. So gilt in AVL – Bäumen für jeden Teilbaum : | Höhe des linken Teilbaumes – Höhe des rechten Teilbaumes | ≤ 1 Durch diese Bedingung erreichen AVL – Bäume maximal eine Höhe von log n im Gegensatz zu normalen binären Suchbäumen, die eine maximale Höhe von n erreichen können. 3. Sicherstellen der AVL – Bedingung Um nun zu gewährleisten, dass diese Höhenbedingung immer eingehalten wird muss nach jeder Veränderung des Baumes, durch Einfügen oder Löschen, überprüft werden, ob die Bedingung noch erfüllt ist. Hierzu überprüft man, von der veränderten Stelle ausgehend, alle Knoten auf dem Pfad zur Wurzel auf ihre Balanciertheit. Ist die Balance-Bedingung verletzt, so muss der Baum rebalanciert werden. Dies geschieht durch eine „Drehung“ im entsprechenden Teilbaum, auch 13 Rotation genannt. Abb. 5 zeigt einen AVL – Baum, in den ein Schlüssel eingefügt wird. Dies führt zu einer Verletzung der Höhenbedingung und der Baum wird durch eine Linksrotation wieder in Balance gebracht. 7 Balance – Bedingung verletzt 7 2 1 21 91 91 insert(6) 14 41 61 7 Linksrotation 41 91 21 61 Abb. 5 : Einfügen in einen AVL – Baum mit Linksrotation In einigen Fällen reicht eine solche, einfache, Rotation nicht aus um die Balance wiederherzustellen. Dann muss noch in einem darüber liegenden Knoten rotiert werden. Die gleichen Mechanismen greifen natürlich auch beim Löschen. Bedingung verletzt 6 7 4 41 7 91 2 21 5 61 51 Doppelrotation links -rechts Abb. 6 : Nach Einfügen der 5 in den Baum findet erst eine Linksrotation um die 4 statt und danach eine Rechtsrotation um die 7. 14 9 4. Implementierung von AVL – Bäumen Um nun aus einem normalen binären Suchbaum einen AVL – Baum zu machen, muss man jedem Knoten einen Balancezeiger geben. Dieser kann folgende Werte annehmen : -1 : der linke Teilbaum ist um 1 höher als der rechte 0 : beide Teilbäume haben dieselbe Höhe 1 : der rechte Teilbaum ist um 1 höher als der linke Wird beim Einfügen oder Entfernen nun ein solcher Zeiger in der Art verändert, dass die Balance nicht mehr stimmt, so muss rotiert werden. Hierzu müssen entsprechende Rotationsmethoden geschrieben werden. 5. Code zur Implementierung von AVL – Bäumen Bei dem folgenden Codefragment handelt es sich um die Methode zur Rebalancierung nach Einfügen im rechten bzw. Entfernen aus dem linken Teilbaum. In beiden Fällen muss gegebenenfalls nach links rebalanciert werden. Die Rebalancierung nach rechts verläuft genau spiegelverkehrt. Diese Methoden kommen immer dann zum Einsatz, wenn es zu einer Bedingungsverletzung gekommen sein kann, weil ein Teilbaum gewachsen bzw. geschrumpft ist. Dies wird durch eine globale flag-Variable beim Einfügen bzw. Löschen gekennzeichnet. // Die Methode wird aufgerufen, wenn die flag – Variable nach dem Entfernen // aus dem linken Teilbaum bzw. dem Einfügen in den rechten Teilbaum auf true // steht. private Node rebalanceLeft(byte balance) { // Der Übersicht halber hier nur der Fall, dass rechts eingefügt wurde. Der // andere Fall ist analog, nur im anderen Teilbaum. switch (balance){ case -1: balance=0; return this; // // // // // Da vorher der linke Teilbaum um 1 höher war als der rechte ist nach dem Einfügen in den rechten Teilbaum kein Höhenunterschied mehr vorhanden. Die flag bleibt auf true. 15 case 0: balance=1; flag=false; return this; case 1: // // // // Da vorher Balance herrschte ist nun der rechte Teilbaum um 1 höher. Dies ist keine Verletzung, daher kann die flag auf false gesetzt werden. // Da der rechte Teilbaum vorher schon um 1 // höher ist die Balance verletzt und es muss // rotiert werden. // Wenn der rechte Teilbaum eine Balance von 0 oder 1 hat muss nach links // rotiert werden um den Höhenunterschied auszugleichen, denn der rechte // Teilbaum ist gewachsen. if (right.balance >=0) { Node current=rotateLeft(); // Ist die Balance genau 0 gewesen, so wird die Balance des durch die // Linksrotation erzeugten Knotens auf –1 gesetzt, da durch die Rotation der // linke Teilbaum gewachsen ist. if (right.balance==0) { current.balance=-1; // Die flag wird false gesetzt, da der Höhenunterschied ausgeglichen wurde. flag=false; return current; } // War die Balance nicht genau 0, sondern 1, so ist die Höhe ausgeglichen else { balance=0; current.balance=0; return current; } } // War die Balance –1 so muss eine Doppelrotation durchgeführt werden. else { byte b=right.left.balance; // Zuerst wird der rechte Teilbaum nach rechts rotiert, da hier ja ein Ungleichgewicht nach links besteht. Danach wird der aktuelle Knoten nach links rotiert, da ja ein Ungleichgewicht nach rechts besteht. right=right.rotateRight(); Node current=rotateLeft(); // Zum Schluss werden noch die Balancen wieder richtig gesetzt. current.balance=0; current.right.balance=(byte)((b==-1)? 1:0); current.left.balance=(byte)((b==1)? -1:0); return current; } } } 16 V. Anwendungsbeispiel : Implementierung einer dünn besetzten Matrix Dünn besetzte Matrizen Eine n x n Matrix bezeichnet man als dünn besetzt, wenn nur O (n) viele ihrer Elemente von Null verschieden sind. Man kann also bei der Implementierung viel Speicherplatz sparen, da man nicht eine vollständige n x n Matrix, die extrem viele Nullen enthält, speichern muss, sondern nur die Stellen speichert, an denen sich das Element von Null unterscheidet. Hierzu eignen sich balancierte Suchbäume sehr gut, da man in ihnen schnell Suchen, Einfügen und Entfernen kann. Eine dünn besetzte Matrix lässt sich also sinnvoll als ein AVL – Baum realisieren, der als Knotenobjekte wiederum AVL – Bäumen hat. Die inneren Bäume stellen dabei die Zeilen dar, die wiederum aus den Werten der einzelnen Spalten bestehen. Der Java – Code zur Implementierung einer solchen Matrix findet sich im Anhang. 17 VI. Anhang 1. Literaturverzeichnis Clausing, Achim : Datenstrukturen und Algorithmen, Vorlesungsskript, Westfälische-Wilhelms-Universität Münster, WS 2000/01. Kuchen, Herbert : Praktische Informatik II, Vorlesungsskript, Westfälische-Wilhelms-Universität Münster, SS 1998. Ottmann, Thomas und Widmayer, Peter : Algorithmen und Datenstrukturen. 3. Auflage, Heidelberg/Berlin/Oxford 1996. 2. Code zur Implementierung dünn besetzter Matrizen Zur Implementierung werden AVL – Bäume benötigt. Die hier benutzte Implementierung entspricht weitestgehend der Implementierung aus dem Vorlesungsskript „Praktische Informatik II“. Sie ist hier nur der Vollständigkeit halber aufgeführt. Die Klasse Matrix implementiert eine dünn besetzte Matrix wie in V. beschrieben. Die Klasse Matrixoperationen stellt die Operationen wie Matrixmultiplikation und Matrixaddition zur Verfügung. class AVL { private class Node { int key; Object content; Node left; Node right; byte balance; Node(int k, Object c, Node l, Node r, byte b) { key=k; content=c; left=l; right=r; balance=b; } Node insertNode(int k, Object d){ 18 Node current; if (key<k) { if (right!=null){ right=right.insertNode(k,d); if (flag) { switch(balance) { case -1: balance=0; flag=false; return this; case 0: balance=1; return this; case 1: if (right.balance==1) { current=rotateLeft(); current.left.balance=0; } else { byte grandchildBal=right.left.balance; right=right.rotateRight(); current=rotateLeft(); current.right.balance=(grandchildBal==1)? (byte)0:(byte)1; current.left.balance=(grandchildBal==1)? (byte)-1:(byte)0; } current.balance=0; flag=false; return current; } //switch-Ende } else return this; //if(flag)-Ende } else { //(right==null) right=new Node(k,d,null,null,(byte)0); balance++; flag=(balance>=1); return this; } } else { //(key >= k) if (left!=null) { left=left.insertNode(k,d); if (flag) { switch(balance) { case 1: balance=0; flag=false; return this; case 0: balance= -1; return this; case -1: if (left.balance==-1) { current=rotateRight(); current.right.balance=0; } else { byte b=right.left.balance; left=left.rotateLeft(); current=rotateRight(); current.right.balance=(b==1)? (byte)0:(byte)1; current.left.balance=(b==1)? (byte)-1:(byte)0; } current.balance=0; flag=false; return current; } //switch-Ende } else return this; //if(flag)-Ende } else { //(left==null) left=new Node(k,d,null,null,(byte)0); balance--; flag=(balance<=-1); return this; 19 } } //if(key<k) - Ende return this; } //insertNode-Ende private Node rotateLeft() { Node current=right; right=right.left; current.left=this; return current; } private Node rotateRight() { Node current=left; left=left.right; current.right=this; return current; } Node deleteNode(int k) throws Exception { if (key<k) { right=right.deleteNode(k); if (flag) return rebalanceRight(); } else { if (k<key) { left=left.deleteNode(k); if (flag) return rebalanceLeft(); } else { if (left==null) { flag=true; return right; } else { if (right==null) { flag=true; return left; } else { Node max=left.findMax(); key=max.key; content=max.content; left=left.deleteNode(key); if (flag) return rebalanceLeft(); } } } } return this; } 20 private Node findMax() { if (right==null) return this; else return right.findMax(); } private Node rebalanceRight() { switch (balance) { case 1: balance=0; return this; case 0: balance= -1; flag=false; return this; case -1: byte childBal=left.balance; if (childBal<=0) { Node current=rotateRight(); if (childBal==0) { current.balance=1; flag=false; return current; } else { balance=0; current.balance=0; return current; } } else { byte b=left.right.balance; left=left.rotateLeft(); Node current=rotateRight(); current.balance=0; current.left.balance=(byte)((b==1)?-1:0); current.right.balance=(byte)((b==-1)? 1:0); return current; } } return this; } private Node rebalanceLeft() { switch (balance) { case -1: balance=0; return this; case 0: balance=1; flag=false; return this; case 1: byte childBal=right.balance; if (childBal>=0) { Node current=rotateLeft(); if (childBal==0) { current.balance=-1; flag=false; return current; 21 } else { balance=0; current.balance=0; return current; } } else { byte b=right.left.balance; right=right.rotateRight(); Node current=rotateLeft(); current.balance=0; current.right.balance=(byte)((b==-1)? 1:0); current.left.balance=(byte)((b==1)? -1:0); return current; } } return this; } } protected Node root=null; private boolean flag=false; public AVL(){;} public Object find(int k) throws Exception { return findElem(k,root); } private static Object findElem(int k, Node current) throws Exception { if (current==null) throw new Exception("Nicht gefunden"); if (current.key<k) return findElem(k,current.right); if (k<current.key) return findElem (k, current.left); return current.content; } public void insert(int k, Object d) { if (root==null) root=new Node(k,d,null,null,(byte)0); else root=root.insertNode(k,d); } public void delete(int k) throws Exception { if (root==null) throw new Exception("Nicht gefunden"); else roo t=root.deleteNode(k); } private String showTreeRec(Node current) { if(current==null){return "";} else { return current.content+" "+showTreeRec(current.left)+showTreeRec(current.right); } } public String toString() { return showTreeRec(root); } } 22 import java.io.*; class Matrix { // <<<<<<<<<<<<<<<<<<<<<<<<<<<Konstruktor>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> public Matrix(int zeilenzahl, int spaltenzahl, int[][] elemente) throws Exception{ // Initialisierung matrix=new AVL(); // Die Matrix ist ein AVL - Baum, der // weitere Bäume für die Zeilen enthält. AVL zeilenbaum; try{ // Es werden der Reihe nach alle Zeilen durchgegangen for(int i=0;i<zeilenzahl;i++){ // Für jede Zeile wird ein neuer Zeilenbaum initialisiert zeilenbaum=new AVL(); // Innerhalb dieser Zeile werden nun die Inhalte der einzelnen Spalten gelesen for(int j=0;j<spaltenzahl;j++){ // Enthält die Spalte einen Wert, der sich von 0 unterscheidet, so wird // dieser als Objekt im Zeilenbaum gespeichert. Dort ist er dann unter dem // Schlü ssel seiner Spalte zu finden. if(elemente[i][j]!=0){ zeilenbaum.insert(j,new Integer(elemente[i][j])); } } // Der Zeilenbaum wird unter dem Schlüssel seiner Zeile in // der Matrix gespeichert. matrix.insert(i,zeilenbaum); } // Die Matrix speichert ihre Spalten- und Zeilenzahl. this.spalten=spaltenzahl; this.zeilen=zeilenzahl; // Hat es im Verlauf der Konstruktion ei nen Fehler gegeben, so wird eine Fehler// meldung ausgegeben } catch (Exception e){ throw new Exception("Fehlerhafte Eingabewerte !"); } } // <<<<<<<<<<<<<<<<<<<<<<<<Methode hole>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> // Die Methode hole gibt das Element an der Koordinate x,y aus public int hole(int x, int y) throws Exception{ // Zuerst wird überprüft, ob es sich um eine gültige Koordinate handelt, falls // nicht wird eine entsprechende Fehlermeldung ausgegeben. if((x>=zeilen)||(y>=spalten)){ throw new Exception("Die Koordinate liegt nicht in der Matrix !"); } // Um auf die Werte einer Zeile zugreifen zu können muss der entsprechende // Zeilenbaum bereitstehen. Hierzu konstruiert man einen Hilfsbaum, mit dem // man auf den entsprechenden Zeilenbaum zugreifen kann. AVL hilf=new AVL(); // Um den Wert zu speichern, der ja als Integer-Objekt im Zeilenbaum vorliegt 23 // konstruiert man ein solches Objekt. Integer i=new Integer(0); try{ // In der Matrix wird nun der entsprechende Zeilenbaum gesucht. hilf=(AVL)matrix.find(x); // Innerhalb dieses Zeilenbaumes wird dann der Wert gesucht. i=(Integer)hilf.find(y); } catch (Exception e){ // Wird an der angegebenen Stelle kein Wert gefunden, so handelt es sich um // eine Stelle an der eine 0 steht. return 0; } // Aus dem Integer - Objekt wird eine int Variable gemacht. return i.intValue(); } // <<<<<<<<<<<<<<<<<<<<<<<<Methode ersetze>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> // die Methode ersetze ersetzt einen Wert an der Koordinate x,y durch einen anderen public void ersetze(int x, int y, int neuWert) throws Exception{ // Zuerst wird überprüft, ob es sich um eine gültige Koordinate handelt, falls // nicht wird eine entsprechende Fehlermeldung ausgegeben. if((x>=zeilen)||(y>=spalten)){ throw new Exception("Die Koordinate liegt nicht in der Matrix !"); } // genau wie bei hole. AVL hilf=new AVL(); // Um den neuen Wert im Zeilenbaum zu speichern muss er in ein Integer-Objekt // verwandelt werden. Integer n=new Integer(neuWert); try{ // In der Matrix wird nun der entsprechende Zeilenbaum gesucht. hilf=(AVL)matrix.find(x); // Ist der alte Wert Null, kann er nicht gelöscht werden, da // in dem Fall gar kein entsprechender Eintrag besteht. Sonst // wird der alte Wert gelöscht. if(hole(x,y)!=0){hilf.delete(y);} // Ist der neue Wert Null, so wird er nicht eingefügt, sonst // wird an der entsprechenden Stelle eingefügt. if(n.intValue()!=0) {hilf.insert(y,n);} } catch (Exception e){;} } // <<<<<<<<<<<<<<<<<<<<<<<<<<<<<Matrizen ausgeben>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> // Diese Methode überschreibt die toString Methode von Java um eine // elegantere Ausgabe zu produzieren public String toString(){ // Man erzeugt einen String, in den die Matrix gespecihert werden kann String s=""; // Um auf die Zeilen zugreifen zu können erzeugt, man wieder einen Hilfsbaum AVL hilf=new AVL(); 24 // Nun geht man alle Zeilen durch for(int i=0;i<zeilen;i++){ try{ // und holt sich den entsprechenden Zeilenbaum hilf=(AVL)matrix.find(i); } catch (Exception e){;} // Dann geht man alle Spalten durch for(int j=0;j<spalten;j++){ try{ // und erweitert den Ausgabestring um ein Leerzeichen und den Wert // für die jeweilige Spalte s+=" "+hilf.find(j); } catch (Exception e) { // Wird kein Wert gefunden so steht an der Stelle eine Null s+=" 0"; } } // Am Ende jeder Zeile wird ein Zeilenumbruch durchgeführt s+="\n"; } return s; } // <<<<<<<<<<<<<<<<<<<<<<<Anzahl der Spalten>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> public int spaltenanz(){ return spalten; } // <<<<<<<<<<<<<<<<<<<<<<<Anzahl der Zeilen>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> public int zeilenanz(){ return zeilen; } // <<<<<<<<<<<<<<<<<<<<<<<Attribute von Matrix>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> private AVL matrix; private int zeilen; private int spalten; } 25 class Matrixoperationen{ // <<<<<<<<<<<<<<<<Multiplikation zweier Matrizen>>>>>>>>>>>>>>>>>>>>>> public static Matrix MatrixMult(Matrix a, Matrix b) throws Exception{ // Um zwei Matrizen zu multiplizieren muss die Spaltenanzahl der einen mit der // Zeilenanzahl der anderen übereinstimmen if(a.spaltenanz()!=b.zeilenanz()){ throw new Exception("Multiplikation nicht möglich !"); } // Bei der Multiplikation zweier Matrizen entsteht eine neue Matrix // mit anderen Dimensionen. Daher wird das Ergebnis in ein zweidim. // Array gespeichert, aus dem dann anschließend eine neue Matrix // konstruiert werden kann. int[][] produkt=new int[a.zeilenanz()][b.spaltenanz()]; try{ // Bei der Mutiplikation werden alle Zeilen von a for(int i=0; i<a.zeilenanz();i++){ // mit allen Spalten von b for(int j=0; j<b.spaltenanz();j++){ // multipliziert. Pro Spalte von b also so oft wie a Spalten hat for(int k=0; k<a.spaltenanz();k++){ // Das Ergebnis wird aufaddiert und gespeichert. produkt[i][j]+=a.hole(i,k)*b.hole(k,j); } } } // Es wird eine neue Matrix konstruiert return new Matrix(a.zeilenanz(),b.spaltenanz(),produkt); } catch (Exception e) {;} return null; } // <<<<<<<<<<<<<<<<<<<<<<<<<<Addition zweier Matrizen>>>>>>>>>>>>>>>>>>>>>>>>>> public static Matrix MatrixAdd(Matrix a, Matrix b) throws Exception{ // Um zwei Matrizen zu addieren müssen sie gleiche Zeilen- und Spaltenanzahl // haben if((a.zeilenanz()!=b.zeilenanz())||(a.spaltenanz()!=b.spaltenanz())){ throw new Exception("Addition nicht möglich !"); } // Es wird eine Kopie der ersten Matrix erstellt, damit die eigentliche Matrix // unverändert bleibt Matrix matrix=MatrixKopie(a); try{ // Für jede Zeile for(int i=0; i<matrix.zeilenanz();i++){ // wird in jeder Spalte for(int j=0; j<matrix.spaltenanz();j++){ // der alte Wert ersetzt, durch die Summe aus den Werten an den // entsprechenden Stellen in Matrix a und b. 26 matrix.ersetze(i,j,(matrix.hole(i,j)+b.hole(i,j))); } } } catch (Exception e) {;} return matrix; } // <<<<<<<<<<<<<<<<<<<Multiplikation mit einem Skalar>>>>>>>>>>>>>>>>>>>>>>>> public static Matrix SkalarMult(Matrix m, int s){ // Es wird eine Kopie der Eingangsmatrix m erstellt, damit diese nicht verändert // wird. Matrix matrix=MatrixKopie(m); try{ // In jeder Zeile for(int i=0; i<matrix.zeilenanz();i++){ // wird in jeder Spalte for(int j=0; j<matrix.spaltenanz();j++){ // überprüft, ob der Wert unterschiedlich von Null ist. if(matrix.hole(i,j)!=0){ // Ist das der Fall, so wird der Wert mit dem Skalar multipliziert // und der neue Wert an die entsprechende Stelle geschrieben. matrix.ersetze(i,j,(s*matrix.hole(i,j))); } } } } catch (Exception e) {;} return matrix; } // <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<Matrix kopieren>>>>>>>>>>>>>>>>>>>>>>>>>>>> private static Matrix MatrixKopie(Matrix m){ // Eine Matrix zu kopiern bedeutet, dass man eine identische Matrix neu erzeugt. // Hierzu erzeugt man zuerst ein zweidim. Array mit den Elementen der Matrix. // Aus diesem kann dann eine neue Matrix konstruiert werden. int[][] elemente=new int[m.zeilenanz()][m.spaltenanz()]; // Aus allen Zeilen for(int i=0; i<m.zeilenanz();i++){ // werden die Werte aller Spalten for(int j=0; j<m.spaltenanz();j++){ try{ // in den Array gespeichert. elemente[i][j]=m.hole(i,j); } catch (Exception e){;} } } try{ // Anschließend wird daraus die neue Matrix konstruiert. return new Matrix(m.zeilenanz(),m.spaltenanz(),elemente); } catch (Exception e) {;} return null; } 27 // <<<<<<<<<<<<<<<<<<<<<<<<<<<<<Matrix transponieren>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> public static Matrix MatrixTrans(Matrix m){ // Eine Matrix zu transponieren bedeutet ihre Spalten und Zeilen zu // vertauschen. Sie also quasi "auf die Seite zu legen". Hierzu kann man sie // einfach kopieren und dabei Zeilen und Spalten vertauschen. Der Rest ist analog // zum kopieren. int[][] elemente=new int[m.spaltenanz()][m.zeilenanz()]; for(int i=0; i<m.zeilenanz();i++){ for(int j=0; j<m.spaltenanz();j++){ try{ elemente[j][i]=m.hole(i,j); } catch (Exception e){;} } } try{ return new Matrix(m.spaltenanz(),m.zeilenanz(),elemente); } catch (Exception e) {;} return null; } // <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<MAIN>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> public static void main(String args[]) throws Exception{ // Erstellen von zwei Arrays, aus denen die Matrizen konstruiert werden können int[][] mat1=new int[4][3]; int[][] mat2=new int[3][4]; mat1[0][2]=2; mat1[1][1]=3; mat1[2][2]=3; mat1[3][0]=1; mat2[0][2]=3; mat2[1][1]=2; mat2[1][3]=3; mat2[2][0]=1; try{ // Es werden zwei Matrizen erstellt Matrix m1=new Matrix(4,3,mat1); Matrix m2=new Matrix(3,4,mat2); // und ausgegeben. System.out.println("Matrix 1:\n"+m1); System.out.println("Matrix 2:\n"+m2); // Die Matrizen werden multipliziert System.out.println("Matrix 1 x Matrix 2: \n"+MatrixMult(m1,m2)); // Um die Matrizen addieren zu können muss Matrix 2 erst transponiert werden System.o ut.println("Matrix 2 in transponierter Form:\n"+MatrixTrans(m2)); // Dann werden die Matrizen addiert System.out.println("Matrix 1 + Matrix 2: \n"+MatrixAdd(m1,MatrixTrans(m2))); // Matrix 1 wird mit einem Skalar multipliziert System.out.println("Skalarmult. von Matrix 1 mit 3:\n"+SkalarMult(m1,3)); } catch (Exception e){System.out.println(e);} } } 28