Einleitung
Dieser Artikel widmet sich BinĂ€rsuchbĂ€umen. KĂŒrzlich habe ich einen Artikel ĂŒber Dabei habe ich nicht viel Aufmerksamkeit auf BinĂ€rbĂ€ume gelegt, da die Methoden fĂŒr Suche, EinfĂŒgen und Löschen nicht relevant waren. Jetzt habe ich beschlossen, einen Artikel genau ĂŒber BĂ€ume zu schreiben. Fangen wir an.
Ein Baum ist eine Datenstruktur, die aus Knoten besteht, die durch Kanten verbunden sind. Man kann sagen, dass ein Baum eine spezielle Form eines Graphen ist. Hier ist ein Beispiel fĂŒr einen Baum:

Das ist kein BinÀrsuchbaum! Alle Details im weiterhin genannten Text!
Terminologie
Wurzel
Die Wurzel des Baums ist der oberste Knoten. Im Beispiel ist das der Knoten A. Im Baum kann es von der Wurzel zu jedem anderen Knoten nur einen einzigen Pfad geben! TatsÀchlich kann jeder Knoten als Wurzel des entsprechenden Teilbaums betrachtet werden.
Eltern/Kinder
Alle Knoten, auĂer der Wurzel, haben genau eine Kante, die nach oben zu einem anderen Knoten fĂŒhrt. Der Knoten, der sich ĂŒber dem aktuellen befindet, wird als Elternteil dieses Knotens bezeichnet. Der Knoten, der sich unter dem aktuellen befindet und mit ihm verbunden ist, wird als Kind dieser Knoten. Lassen Sie uns ein Beispiel nehmen. Nehmen wir Knoten B, dann wird sein Elternteil Knoten A sein, und die Nachkommen sind die Knoten D, E und F.
Blatt
Ein Knoten, der keine Nachkommen hat, wird als Blatt des Baumes bezeichnet. In diesem Beispiel sind die BlÀtter die Knoten D, E, F, G, I, J, K.
Das ist die grundlegende Terminologie. Weitere Konzepte werden spĂ€ter behandelt. Ein binĂ€rer Baum ist ein Baum, in dem jeder Knoten nicht mehr als zwei Nachkommen hat. Wie Sie sich denken können, ist der Baum aus dem Beispiel kein binĂ€rer Baum, da die Knoten B und H mehr als zwei Nachkommen haben. Hier ist ein Beispiel fĂŒr einen binĂ€ren Baum:

In den Knoten des Baumes kann jede Art von Informationen gespeichert werden. Ein binÀrer Suchbaum ist ein binÀrer Baum, der folgende Eigenschaften hat:
- Beide TeilbĂ€ume â der linke und der rechte â sind binĂ€re SuchbĂ€ume.
- FĂŒr alle Knoten des linken Teilbaums eines beliebigen Knotens X sind die Werte der SchlĂŒsseldaten kleiner als der Wert des SchlĂŒssels selbst von Knoten X.
- FĂŒr alle Knoten des rechten Teilbaums eines beliebigen Knotens X sind die Werte der SchlĂŒsseldaten gröĂer oder gleich dem Wert des SchlĂŒssels selbst von Knoten X.
Der SchlĂŒssel â eine Eigenschaft des Knotens (zum Beispiel eine Zahl). Der SchlĂŒssel wird benötigt, um das entsprechende Element des Baumes zu finden. Beispiel eines binĂ€ren Suchbaums:

Darstellung des Baumes
Im Verlauf werde ich einige (möglicherweise unvollstĂ€ndige) Code-Schnipsel anfĂŒhren, um Ihr VerstĂ€ndnis zu verbessern. Den vollstĂ€ndigen Code finden Sie am Ende des Artikels.
Ein Baum besteht aus Knoten. Die Struktur eines Knotens:
public class Node<T> {
private T data;
private int key;
private Node<T> leftChild;
private Node<T> rightChild;
public Node(T data, int key) {
this.data = data;
this.key = key;
}
public Node<T> getLeftChild() {
return leftChild;
}
public Node<T> getRightChild() {
return rightChild;
}
//...weitere Methoden des Knotens
}
Jeder Knoten hat zwei Nachkommen (es ist durchaus möglich, dass die Nachkommen leftChild und/oder rightChild den Wert null enthalten). Sie haben wahrscheinlich verstanden, dass in diesem Fall die Zahl data â die Daten sind, die im Knoten gespeichert sind; key â der SchlĂŒssel des Knotens.
Jetzt, wo wir den Knoten behandelt haben, sprechen wir ĂŒber aktuelle Probleme im Zusammenhang mit BĂ€umen. Hier und im Folgenden beziehe ich mich auf den Begriff des binĂ€ren Suchbaums. Die Struktur eines binĂ€ren Baums:
public class BinaryTree<T> {
private Node<T> root;
// Methoden des Baumes
}
Als Klassenfeld benötigen wir nur die Wurzel des Baumes, da wir ĂŒber die Methoden getLeftChild() und getRightChild() von der Wurzel zu jedem Knoten im Baum gelangen können.
Algorithmen im Baum
Suche
Angenommen, Sie haben einen aufgebauten Baum. Wie finden Sie das Element mit dem SchlĂŒssel key? Sie mĂŒssen sich schrittweise von der Wurzel nach unten bewegen und den Wert key mit dem SchlĂŒssel des aktuellen Knotens vergleichen: Ist key kleiner als der SchlĂŒssel des aktuellen Knotens, wechseln Sie zum linken Kind dieses Knotens; ist er gröĂer, zum rechten; wenn die SchlĂŒssel gleich sind, wurde der gesuchte Knoten gefunden! Der entsprechende Code lautet:
public Node<T> find(int key) {
Node<T> current = root;
while (current.getKey() != key) {
if (key < current.getKey())
current = current.getLeftChild();
else
current = current.getRightChild();
if (current == null)
return null;
}
return current;
}
Wenn current null wird, bedeutet das, dass die Durchsuchung das Ende des Baumes erreicht hat (auf konzeptioneller Ebene befinden Sie sich an einem nicht existierenden Ort im Baum â einem Nachkommen des Blattes).
Betrachten wir die Effizienz des Suchalgorithmus in einem balancierten Baum (einem Baum, in dem die Knoten mehr oder weniger gleichmĂ€Ăig verteilt sind). Die Sucheffizienz betrĂ€gt dann O(log(n)), wobei der Logarithmus zur Basis 2 verwendet wird. Sehen Sie: Wenn ein balancierter Baum n Elemente hat, bedeutet das, dass es log(n) Ebenen im Baum gibt, die zur Basis 2 berechnet werden. Bei der Suche steigen Sie in jedem Schritt der Schleife eine Ebene hinunter.
EinfĂŒgen
Wenn Sie das Prinzip der Suche verstanden haben, wird es Ihnen leichtfallen, das EinfĂŒgen zu verstehen. Sie mĂŒssen einfach zum Blatt des Baumes hinuntersteigen (den Regeln des Abstiegs folgen, die in der Suche beschrieben sind) und dort Nachkommen werden â entweder links oder rechts, je nach SchlĂŒssel. Implementierung:
public void insert(T insertData, int key) {
Node current = root;
Node parent;
Node newNode = new Node(insertData, key);
if (root == null)
root = newNode;
else {
while (true) {
parent = current;
if (key < current.getKey()) {
current = current.getLeftChild();
if (current == null) {
parent.setLeftChild(newNode);
return;
}
}
else {
current = current.getRightChild();
if (current == null) {
parent.setRightChild(newNode);
return;
}
}
}
}
}
In diesem Fall mĂŒssen wir neben dem aktuellen Knoten auch Informationen ĂŒber den Elternknoten speichern. Wenn 'current' null ist, wird die benötigte Liste in der Variablen 'parent' gespeichert.
Die Effizienz der EinfĂŒgung wird offensichtlich die gleiche sein wie die Suche â O(log(n)).
Entfernen
Das Löschen ist die komplizierteste Operation, die wir mit dem Baum durchfĂŒhren mĂŒssen. ZunĂ€chst mĂŒssen wir das Element finden, das wir löschen möchten. Aber was kommt danach? Wenn wir seiner Referenz einfach den Wert null zuweisen, verlieren wir die Informationen ĂŒber den Teilbaum, dessen Wurzel dieser Knoten ist. Die Methoden zum Löschen in einem Baum werden in drei FĂ€lle unterteilt.
Erster Fall. Der zu löschende Knoten hat keine Nachfolger.
Wenn der zu löschende Knoten keine Nachfolger hat, bedeutet das, dass er ein Blatt ist. Folglich können wir einfach den Feldern 'leftChild' oder 'rightChild' seines Elternteils den Wert null zuweisen.
Zweiter Fall. Der zu löschende Knoten hat einen Nachfolger.
Dieser Fall ist ebenfalls nicht sehr kompliziert. Lassen Sie uns zu unserem Beispiel zurĂŒckkehren. Angenommen, wir mĂŒssen das Element mit dem SchlĂŒssel 14 entfernen. Sie stimmen zu, dass es, da es das rechte Kind des Knotens mit dem SchlĂŒssel 10 ist, jedes seiner Nachkommen (in diesem Fall das rechte) einen SchlĂŒssel haben wird, der gröĂer als 10 ist. Daher können wir es leicht aus dem Baum «herausnehmen» und den Elternknoten direkt mit dem Nachkommen des zu löschenden Knotens verbinden, d. h. den Knoten mit dem SchlĂŒssel 10 mit dem Knoten 13 verbinden. Eine Ă€hnliche Situation wĂŒrde auftreten, wenn wir einen Knoten löschen mĂŒssten, der das linke Kind seines Elternteils ist. Denken Sie selbst darĂŒber nach â die genaue Analogie.
Der dritte Fall. Der Knoten hat zwei Nachkommen
Der komplizierteste Fall. Lassen Sie uns ein neues Beispiel durchgehen.

Nachfolger suchen
Angenommen, wir mĂŒssen den Knoten mit dem SchlĂŒssel 25 entfernen. Wen setzen wir an seine Stelle? Jemand aus seinen Nachkommen (Nachkommen oder Nachkommen von Nachkommen) muss Nachfolger(die Person, die den Platz des zu löschenden Knotens einnimmt).
Wie ermitteln Sie den Nachfolger? Es ist intuitiv klar, dass dies der Knoten im Baum ist, dessen SchlĂŒssel der nĂ€chstgröĂere vom zu entfernenden Knoten ist. Der Algorithmus sieht wie folgt aus: Sie mĂŒssen zu seinem rechten Nachkommen gehen (immer zum rechten, denn, wie bereits erwĂ€hnt, ist der SchlĂŒssel des Nachfolgers gröĂer als der des zu entfernenden Knotens) und dann die Kette der linken Nachkommen dieses rechten Nachkommen durchlaufen. Im Beispiel mĂŒssen wir zum Knoten mit dem SchlĂŒssel 35 gehen und dann nach unten die Kette seiner linken Nachkommen bis zum Blatt durchlaufen â in diesem Fall besteht diese Kette nur aus dem Knoten mit dem SchlĂŒssel 30. Streng genommen suchen wir das kleinste Element in der Menge der Knoten, die gröĂer als der gesuchte Knoten sind.

Code zur Methode der Nachfolgersuche:
public Node getSuccessor(Node deleteNode) {
Node parentSuccessor = deleteNode; // Elternteil des Nachfolgers
Node successor = deleteNode; // Nachfolger
Node current = successor.getRightChild(); // einfach "laufender" Knoten
while (current != null) {
parentSuccessor = successor;
successor = current;
current = current.getLeftChild();
}
// beim Verlassen der Schleife haben wir den Nachfolger und den Elternteil des Nachfolgers
if (successor != deleteNode.getRightChild()) { // wenn der Nachfolger nicht mit dem rechten Kind des zu löschenden Knotens ĂŒbereinstimmt
parentSuccessor.setLeftChild(successor.getRightChild()); // dann ĂŒbernimmt der Elternteil den Nachfolger, um dessen Kind nicht zu verlieren
successor.setRightChild(deleteNode.getRightChild()); // verbinden den Nachfolger mit dem rechten Kind des zu löschenden Knotens
}
return successor;
}
Der vollstÀndige Code der Methode delete:
public boolean delete(int deleteKey) {
Node current = root;
Node parent = current;
boolean isLeftChild = false; // Je nach dem, ob der zu löschende Knoten ein linkes oder rechtes Kind seines Elternteils ist, wird die boolesche Variable isLeftChild entsprechend true oder false sein.
while (current.getKey() != deleteKey) {
parent = current;
if (deleteKey < current.getKey()) {
current = current.getLeftChild();
isLeftChild = true;
} else {
isLeftChild = false;
current = current.getRightChild();
}
if (current == null)
return false;
}
if (current.getLeftChild() == null && current.getRightChild() == null) { // erster Fall
if (current == root)
current = null;
else if (isLeftChild)
parent.setLeftChild(null);
else
parent.setRightChild(null);
}
else if (current.getRightChild() == null) { // zweiter Fall
if (current == root)
root = current.getLeftChild();
else if (isLeftChild)
parent.setLeftChild(current.getLeftChild());
else
current.setRightChild(current.getLeftChild());
} else if (current.getLeftChild() == null) {
if (current == root)
root = current.getRightChild();
else if (isLeftChild)
parent.setLeftChild(current.getRightChild());
else
parent.setRightChild(current.getRightChild());
}
else { // dritter Fall
Node successor = getSuccessor(current);
if (current == root)
root = successor;
else if (isLeftChild)
parent.setLeftChild(successor);
else
parent.setRightChild(successor);
}
return true;
}
Die KomplexitÀt kann durch O(log(n)) approximiert werden.
Suche nach dem Maximum/Minimum im Baum
Um den minimalen/maximalen Wert im Baum zu finden, muss man gemÀà der Kette der linken/rechten Elemente des Baums nacheinander vorgehen; wenn man ein Blatt erreicht, ist es das minimale/maximale Element.
public Node<T> getMinimum(Node<T> startPoint) {
Node<T> current = startPoint;
Node<T> parent = current;
while (current != null) {
parent = current;
current = current.getLeftChild();
}
return parent;
}
public Node<T> getMaximum(Node<T> startPoint) {
Node<T> current = startPoint;
Node<T> parent = current;
while (current != null) {
parent = current;
current = current.getRightChild();
}
return parent;
}
Die KomplexitÀt betrÀgt O(log(n)).
Symmetrische Traversierung
Traversierung bedeutet, jeden Knoten im Baum zu besuchen, um eine bestimmte Aktion mit ihm durchzufĂŒhren.
Algorithmus fĂŒr die rekursive symmetrische Traversierung:
- Aktion mit dem linken Nachkommen durchfĂŒhren
- Aktion mit sich selbst durchfĂŒhren
- Aktion mit dem rechten Nachkommen durchfĂŒhren
Code:
public void inOrder(Node<T> current) {
if (current != null) {
inOrder(current.getLeftChild());
System.out.println(current.getData() + " ");//Hier kann alles Mögliche stehen
inOrder(current.getRightChild());
}
}
Fazit
Endlich! Wenn ich etwas nicht klar genug erklÀrt habe oder Sie Anmerkungen haben, lassen Sie es mich in den Kommentaren wissen. Wie versprochen, hier ist der vollstÀndige Code.
Node.java:
public class Node {
private T data;
private int key;
private Node leftChild;
private Node rightChild;
public Node(T data, int key) {
this.data = data;
this.key = key;
}
public void setLeftChild(Node newNode) {
leftChild = newNode;
}
public void setRightChild(Node newNode) {
rightChild = newNode;
}
public Node getLeftChild() {
return leftChild;
}
public Node getRightChild() {
return rightChild;
}
public T getData() {
return data;
}
public int getKey() {
return key;
}
}
BinaryTree.java:
public class BinaryTree {
private Node root;
public Node find(int key) {
Node current = root;
while (current.getKey() != key) {
if (key < current.getKey())
current = current.getLeftChild();
else
current = current.getRightChild();
if (current == null)
return null;
}
return current;
}
public void insert(T insertData, int key) {
Node current = root;
Node parent;
Node newNode = new Node(insertData, key);
if (root == null)
root = newNode;
else {
while (true) {
parent = current;
if (key < current.getKey()) {
current = current.getLeftChild();
if (current == null) {
parent.setLeftChild(newNode);
return;
}
}
else {
current = current.getRightChild();
if (current == null) {
parent.setRightChild(newNode);
return;
}
}
}
}
}
public Node getMinimum(Node startPoint) {
Node current = startPoint;
Node parent = current;
while (current != null) {
parent = current;
current = current.getLeftChild();
}
return parent;
}
public Node getMaximum(Node startPoint) {
Node current = startPoint;
Node parent = current;
while (current != null) {
parent = current;
current = current.getRightChild();
}
return parent;
}
public Node getSuccessor(Node deleteNode) {
Node parentSuccessor = deleteNode;
Node successor = deleteNode;
Node current = successor.getRightChild();
while (current != null) {
parentSuccessor = successor;
successor = current;
current = current.getLeftChild();
}
if (successor != deleteNode.getRightChild()) {
parentSuccessor.setLeftChild(successor.getRightChild());
successor.setRightChild(deleteNode.getRightChild());
}
return successor;
}
public boolean delete(int deleteKey) {
Node current = root;
Node parent = current;
boolean isLeftChild = false;
while (current.getKey() != deleteKey) {
parent = current;
if (deleteKey < current.getKey()) {
current = current.getLeftChild();
isLeftChild = true;
} else {
isLeftChild = false;
current = current.getRightChild();
}
if (current == null)
return false;
}
if (current.getLeftChild() == null && current.getRightChild() == null) {
if (current == root)
current = null;
else if (isLeftChild)
parent.setLeftChild(null);
else
parent.setRightChild(null);
}
else if (current.getRightChild() == null) {
if (current == root)
root = current.getLeftChild();
else if (isLeftChild)
parent.setLeftChild(current.getLeftChild());
else
current.setRightChild(current.getLeftChild());
} else if (current.getLeftChild() == null) {
if (current == root)
root = current.getRightChild();
else if (isLeftChild)
parent.setLeftChild(current.getRightChild());
else
parent.setRightChild(current.getRightChild());
}
else {
Node successor = getSuccessor(current);
if (current == root)
root = successor;
else if (isLeftChild)
parent.setLeftChild(successor);
else
parent.setRightChild(successor);
}
return true;
}
public void inOrder(Node current) {
if (current != null) {
inOrder(current.getLeftChild());
System.out.println(current.getData() + " ");
inOrder(current.getRightChild());
}
}
}
P.S.
Degeneration zu O(n)
Viele von Ihnen haben vielleicht bemerkt: Was wĂ€re, wenn wir das Baum unbalanciert machen? Zum Beispiel, indem wir Knoten mit aufsteigenden SchlĂŒsseln einfĂŒgen: 1, 2, 3, 4, 5, 6⊠Dann Ă€hnelt der Baum eher einer verketteten Liste. Ja, der Baum verliert seine baumartige Struktur und damit die Effizienz beim Datenzugriff. Die KomplexitĂ€t der Such-, EinfĂŒge- und Löschoperationen wird die gleiche wie bei einer verketteten Liste sein: O(n). Das ist einer der wichtigsten Nachteile von binĂ€ren BĂ€umen aus meiner Sicht.
Nur registrierte Benutzer können an der Umfrage teilnehmen. Sind Sie an Contour interessiert?
Ich bin noch nicht lange auf Habr, und ich wĂŒrde gerne wissen, ĂŒber welche Themen Sie mehr Artikel sehen möchten?
Datenstrukturen
Algorithmen (DP, Rekursion, Datenkompression usw.)
Anwendung von Datenstrukturen und Algorithmen im echten Leben
Programmierung von Android-Anwendungen in Java
Programmierung von Webanwendungen in Java
2 Benutzer haben abgestimmt. 1 Benutzer hat sich enthalten.
Quelle: habr.com
