Listen und Binäre Bäume

Werbung
Binäre Bäume
Binäre Bäume bestehen aus „Knoten“ („Nodes“), die über „Zeiger“ („Pointer“) mit den anderen Knoten verbunden sind. Den
Startknoten nennt man „Wurzel“ („Root“). Im Folgenden arbeiten wir mit einem Baum, in dem jeder Knoten durch eine
Instanz der Klasse „Node“ repräsentiert wird, die ihrerseits zwei „Kinder“ haben kann (wir nennen sie „pChR“ und „pChL“)
und einen „Vater“ (pParent“ = der übergeordnete Knoten). Der Startknoten wird in einem statischen Feld „pRoot“ in einer
statischen Klasse „All“ gespeichert. Da wir uns im Baum bewegen wollen, gibt es außerdem in der „All“-Klasse ein statisches
Feld „pAkt“, das auf die Instanz des gerade aktuellen Knotens zeigt:
aktueller Knoten (pAkt)
linkes „Kind“ (pChL)
rechts „Kind“ (pChR)
pParent
pParent
Klasse „Node“
public class Node
{
public Node pParent; // Zeiger auf vorhergehenden Knoten
public Node pChR; // Zeiger auf folgenden Knoten, rechter Ast
public Node PChL; // Zeiger auf folgenden Knoten, linker Ast
}
Klasse „All“
public static class All
{
public static Node pRoot; // Zeiger auf Startknoten
public static Node pAkt; // Zeiger auf aktuellen Knoten
}
1) Neuen Knoten in einen Baum einfügen
pChL
pAkt
pParent
nNeu
Im Folgenden wird das Einfügen eines neuen Knoten links unter dem aktuellen Knoten gezeigt. Wir erzeugen als erstes
einen neuen Knoten und merken ihn uns in einer lokalen Hilfsvariable:
Node nNeu = new Node(); // neuen Knoten erzeugen und in der Hilfsvariable „nNeu“ merken
Der neue Knoten soll zwischen dem aktuellen und dessen linkem Kind eingeschoben werden. Daher müssen wir seine Pointer
entsprechend setzen: pParent hinauf auf den aktuellen Knoten (pAkt), und pChL hinunter auf dessen linkes „Kind“ (pChR des
neuen bleibt null). Allerdings muss zuvor überprüft werden, ob es überhaupt einen aktuellen Knoten gibt (d.h. ob pAkt !=
null), denn möglicherweise ist unser neuer Knoten der erste und somit der Wurzelknoten:
if (All.pAkt != null) // gibt´s überhaupt einen aktuellen Knoten?
{
nNeu.PChL = pAkt.pChL; // das linke Kind des aktuellen wird zum linken Kind des neuen
nNeu.pParent = All.pAkt; // der aktuelle wird zum Parent des neuen
}
else
{
All.pRoot = pNeu; // falls es noch gar keinen Knoten gibt, wird unser neuer der Wurzelknoten des Baums
}
pChL
pAkt
pParent des neuen Knotens auf den aktuellen
Knoten hinaufzeigen lassen
pParent
nNeu
pChR des neuen Knotens
bleibt null
pChL des neuen Knotens auf den pChL des
aktuellen Knoten hinunterzeigen lassen
Die Pointer des neuen Knoten zeigen jetzt auf die richtigen Nachbarn (oben und unten), aber diese noch nicht auf ihn. Um ihn
in den Baum einzuhängen, müssen wir zwei Zeiger seiner Nachbar auf ihn „verbiegen“:
a) den pChL des aktuellen (hinunter auf den neuen)
b) den pParent vom Kind des aktuellen (hinauf auf den neuen)
Dabei ist zu überprüfen, ob der aktuelle Knoten überhaupt ein Kind besitzt, denn von einem nicht existierenden können wir
natürlich keinen Zeiger bearbeiten:
if (pChL != null)
{
pChL.pParent = nNeu;
}
All.pAkt.PChL = nNeu;
pChL
pAkt
pChL des aktuellen Knotens
auf den neuen hinunterzeigen
lassen
pParent
nNeu
null
Fertig:
pAkt
pChL
pParent des Kindes des aktuellen Knotens
auf den neuen hinaufzeigen lassen
pParent
pNeu
pChL
pParent
null
Zuletzt den neuen Knoten zum aktuellen machen:
All.pAkt = nNeu; // der neue Knoten ist ab jetzt der aktuelle Knoten
2) Element aus einem Baum entfernen
Wenn die Nachbarn des aktuellen Knotens ihre Zeiger von ihm „wegbiegen“, hängt er in der Luft und hat keine Verbindung
mehr zum Baum. Allerdings ist auf die Neuzuordnung dieser Zeiger zu achten:
if (All.pAkt != null) // gibt´s überhaupt einen aktuellen Knoten?
{
if (All.pAkt.pParent != null) // hat der aktuelle Knoten einen Parent?
{
Node nP = All.pAkt.pParent; // Hilfsvariable für den übersichtlicheren Zugriff auf den Parent
if (All.pAkt == nP.pChR) nP.pChR = null; // ist der aktuelle Knoten das rechte Kind seines Parents?
- dann dessen pChR auf null setzen
else nP.PChL = null; // andernfalls dessen pChL auf null setzen
All.pAkt = nP; // der Parent des aktuellen wird zum neuen aktuellen Knoten
}
else
{
All.pRoot = null; // wenn der aktuelle keinen Parent hat, ist er der Wurzelknoten. Nach dessen Entfernen
gibt´s keinen mehr ...
All.pAkt = null; // ... und auch keinen aktuellen
}
}
3) Einen Baum traversieren
Um alle Knoten eines Baumes zu zeichnen, genügt keine einfache Schleife, denn nach jedem Knoten kann sich der Baum
verzweigen. Man benutzt daher „rekursive“ Methoden, um Bäume zu durchlaufen („traversieren“).
Rekursion bedeutet, dass eine Methode sich selbst aufruft.
Im Eventhandler der PictureBox rufen wir eine solche rekursive Methode auf, die 4 Parameter erwartet: das Graphics-Objekt,
den Wurzelknoten und die Koordinaten dieses Wurzelknotens (hier: oben Mitte der PictureBox).
private void pBox_Paint(object sender, PaintEventArgs e)
{
int x = pBox.Width / 2; // Mitte der PictureBox
int y = 8; // Abstand vom oberen Rand
if (All.pRoot != null)
{
knotenRekZeichnen(e.Graphics, All.pRoot, x, y); // rekursive Methode aufrufen
}
}
Die rekursive Methode überprüft als erstes, ob der übergeben Knoten gültig (!= null) ist und ruft sich anschließend
zweimal selbst auf: einmal für den rechten Ast (x-Koord wird erhöht), einmal für den linken Ast (x-Koord wird verringert).
Erst danach erfolgt der eigentliche Zeichnen-Befehl (hier: Aufruf einer entsprechenden Methode in der Klasse Node):
private void knotenRekZeichnen(Graphics g, Node n, int x, int y)
{
if (n != null)
{
knotenRekZeichnen(g, n.pChR, x + 32, y + 32); // rechten Ast durchlaufen
knotenRekZeichnen(g, n.PChL, x - 32, y + 32); // linken Ast durchlaufen
n.zeichnen(g, x, y); // Aufruf der eigentlichen Zeichnen-Methode in der Klasse Node
}
}
Gezeichnet wird in diesem Fall von unten nach oben, weil der Baum zuerst bis zum Ende eines Astes durchlaufen wird, bevor
die Zeichnen-Methode zum Zug kommt.
Wie funktioniert die Rekursion?
Jedes Programm bekommt bei seinem Start einen speziellen, kleinen Speicherbereich zugewiesen (den „Stack“ oder „Stapel“),
der von zwei Zeigern verwaltet wird: einer zeigt auf den Beginn des Stacks, ein zweiter (der „Stackpointer“ - SP) auf die
aktuelle Position im Stack. Bei jedem Sprung in eine Methode werden an der aktuellen Position des Stacks drei Informationen
abgelegt:
 die „Rücksprungadresse“ (d.h. die Stelle im Quellcode, von der in die Methode gesprungen wurde), damit das Programm
nach Beendigung der Methode wieder zurückfindet
 die Parameter, die der Methode mitgegeben werden
 die lokalen Variablen, falls vorhanden
Dieser Bereich auf dem Stack wird erst nach Beendigung der Methode (durch Verschieben des „Stackpointers“) ungültig.
Genau das machen wir uns zu Nutze, indem wir die Methode nicht sofort verlassen, sondern sie solange immer wieder mit
dem nächsten Zeiger (PChR bzw. PChL) aufrufen, bis wir am Ende eines „Astes“ des Baumes angelangt sind (d.h. bis PChR
bzw. PChR == null). Auf diese Weise bleiben die Pointer eines jeden Knoten erhalten (für jeden Knoten wurde ein neuer
Block auf dem Stack angelegt und noch nicht gelöscht, da wir die Methode ja noch nicht verlassen haben).
Herunterladen