Binäre Suchbäume

Werbung
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
Herunterladen