Knotenorientierte und blattorientierte Bäume - oth

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