Predigra
Ta členek je věnován binárním vyhledávacím stromům. Nedávno jsem psal článek o Tam jsem nevěnoval mnoho pozornosti binárním stromům, protože metody vyhledávání, vkládání a mazání nebyly aktuální. Nyní jsem se rozhodl napsat článek právě o stromech. Pojďme začít.
Strom je datová struktura složená z uzlů, které jsou spojeny hranami. Lze říci, že strom je zvláštním případem grafu. Zde je příklad stromu:

To není binární vyhledávací strom! Vše bude pod štítkem!
Terminologija
Kořen
Kořen stromu je nejvyšší uzel. V příkladu je to uzel A. Ve stromě může od kořene k jakémukoli jinému uzlu vést pouze jedna cesta! Ve skutečnosti může být jakýkoli uzel považován za kořen příslušného podstromu tohoto uzlu.
Rodiče/děti
Všechny uzly kromě kořenového mají přesně jednu hranu, která vede nahoru k jinému uzlu. Uzel umístěný výše než aktuální uzel se nazývá rodič tohoto uzlu. Uzel umístěný níže než aktuální, a spojený s ním, se nazývá potomek tohoto uzlu. Podívejme se na příklad. Vezměme uzel B, pak jeho rodičem bude uzel A a potomky budou uzly D, E a F.
Sheet
Uzel, který nemá žádné potomky, se nazývá list stromu. V příkladu jsou listy uzly D, E, F, G, I, J, K.
Toto jsou hlavní termíny. Další pojmy budou vysvětleny později. Takže binární strom je strom, ve kterém má každý uzel maximálně dva potomky. Jak jste asi uhodli, strom z příkladu není binární, protože uzly B a H mají více než dva potomky. Zde je příklad binárního stromu:

U uzlů stromu může být jakákoli informace. Binární vyhledávací strom je binární strom, který má následující vlastnosti:
- Oba podstromy – levý i pravý – jsou binární vyhledávací stromy.
- U všech uzlů levého podstromu libovolného uzlu X jsou hodnoty klíčů dat menší než hodnota klíče dat samotného uzlu X.
- U všech uzlů pravého podstromu libovolného uzlu X jsou hodnoty klíčů dat větší nebo rovny, než hodnota klíče dat samotného uzlu X.
Opcija – nějaká charakteristika uzlu (například číslo). Klíč je potřebný k tomu, abychom mohli najít prvek stromu, kterému tento klíč odpovídá. Příklad binárního vyhledávacího stromu:

Zobrazení stromu
Med napredovanjem bom predstavil nekatere (morda nepopolne) kose kode, da bi izboljšal vaše razumevanje. Celotna koda bo na koncu članka.
Drevo je sestavljeno iz vozlišč. Struktura vozlišča:
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;
}
//...ostali metod vozlišča
}
Vsako vozlišče ima dva potomca (zelo verjetno, da bodo potomca leftChild in/ali rightChild vsebovala vrednost null). Verjetno ste razumeli, da v tem primeru data predstavlja podatke, shranjene v vozlišču; key pa je ključ vozlišča.
Z vozliščem smo se že seznanili, zdaj pa se pogovorimo o trenutnih težavah z drevesi. Tukaj in naprej bom pod izrazom 'drevo' razumel pojem binarnega iskalnega drevesa. Struktura binarnega drevesa:
public class BinaryTree<T> {
private Node<T> root;
//metodi drevesa
}
Kot član razreda bomo potrebovali le koren drevesa, saj lahko od korena z metodama getLeftChild() in getRightChild() prispemo do katerega koli vozlišča drevesa.
Algoritmi v drevesu
Iskanje
Recimo, da imate zgrajeno drevo. Kako najti element s ključem key? Treba je zaporedno premikati od korena navzdol po drevesu in primerjati vrednost key s ključem trenutnega vozlišča: če je key manjši od ključa trenutnega vozlišča, preidite na levega potomca vozlišča, če je večji — na desnega, če sta ključa enaka — iskano vozlišče je najdeno! Ustrezna koda je:
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;
}
Če current postane enak null, pomeni, da je iskanje doseglo konec drevesa (na konceptualni ravni ste v neobstoječem delu drevesa — potomcu lista).
Poglejmo učinkovitost iskalnega algoritma na uravnoteženem drevesu (drevesu, kjer so vozlišča razporejena bolj ali manj enakomerno). Takrat bo učinkovitost iskanja O(log(n)), pri čemer je logaritem pri osnovi 2. Poglejte: če ima v uravnoteženem drevesu n elementov, to pomeni, da bo log(n) pri osnovi 2 nivojev drevesa. Med iskanjem, v enem koraku zanke, se spustite za en nivo.
Vstavljanje
Če ste ujeli bistvo iskanja, vam vstavljanje ne bo predstavljalo težav. Preprosto se morate spustiti do lista drevesa (po pravilih spuščanja, opisanih v iskanju) in postati njegov potomček - levi ali desni, odvisno od ključa. Izvedba:
public void insert(T insertData, int key) {
Node<T> current = root;
Node<T> parent;
Node<T> 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;
}
}
}
}
}
V tem primeru je poleg trenutnega vozlišča potrebno shraniti tudi informacije o staršu trenutnega vozlišča. Ko current postane enak null, bo v spremenljivki parent ležala potrebna igralna plošča.
Učinkovitost vstavljanja bo očitno enaka kot pri iskanju - O(log(n)).
Odstranjevanje
Izbris je najtežja operacija, ki jo je treba izvesti z drevesom. Jasno je, da je najprej treba najti element, ki ga nameravamo izbrisati. Ampak kaj potem? Če preprosto dodelimo njegovemu kazalcu vrednost null, bomo izgubili informacije o poddrevesu, katerega koren je to vozlišče. Metode za brisanje drevesa delimo na tri primere.
Prvi primer. Izbrisano vozlišče nima potomcev.
Če izbrisano vozlišče nima potomcev, to pomeni, da je list. Zato lahko poljem leftChild ali rightChild njegovega starša preprosto dodelimo vrednost null.
Drugi primer. Izbrisano vozlišče ima enega potomca.
Ta primer tudi ni zelo zapleten. Vrnimo se k našemu primeru. Recimo, da je potrebno izbrisati element s ključem 14. Strinjate se, da ker je to desni potomec vozlišča s ključem 10, bo vsak njegov potomec (v tem primeru desni) imel ključ, večji od 10, zato ga lahko zlahka "izrežemo" iz drevesa, starša pa povežemo neposredno s potomcem izbrisanega vozlišča, torej vozlišče s ključem 10 povežemo z vozliščem 13. Podobna situacija bi bila, če bi morali izbrisati vozlišče, ki je levi potomec svojega starša. Razmislite o tem sami - točna analogija.
Tretji primer. Vozlišče ima dva potomca.
Najbolj zapleten primer. Razdelimo na novem primeru.

Iskanje naslednika
Recimo, da je potrebno odstraniti vozlišče s ključno številko 25. Koga bomo postavili na njegovo mesto? Nekdo iz njegovih naslednikov (potomcev ali potomcev potomcev) mora postati naslednik(tisti, ki bo zasedel mesto odstranjene vozlišča).
Kako razumeti, kdo bi moral postati naslednik? Intuitivno razumemo, da je to vozlišče v drevesu, katerega ključ je naslednji največji od odstranjene vozlišča. Algoritem je naslednji. Premakniti se je treba k njegovemu desnemu potomcu (vedno k desnemu, saj je že bilo omenjeno, da je ključ naslednika večji od ključa odstranjene vozlišča), nato pa prehoditi verigo levih potomcev tega desnega potomca. V našem primeru se moramo preseliti k vozlišču s ključno številko 35 in nato prehoditi do lista navzdol po verigi njegovih levih potomcev – v tem primeru ta veriga vsebuje le vozlišče s ključno številko 30. Strogo gledano, iščemo najmanjše vozlišče v skušini vozlišč, ki so večja od iskane vozlišča.

Koda metode iskanja naslednika:
public Node getSuccessor(Node deleteNode) {
Node parentSuccessor = deleteNode; // starš naslednika
Node successor = deleteNode; // naslednik
Node current = successor.getRightChild(); // preprosto 'prehajajoče' vozlišče
while (current != null) {
parentSuccessor = successor;
successor = current;
current = current.getLeftChild();
}
// ob izhodu iz cikla imamo naslednika in starša naslednika
if (successor != deleteNode.getRightChild()) { // če se naslednik ne ujema s desnim potomcem odstranjene vozlišča
parentSuccessor.setLeftChild(successor.getRightChild()); // njegov starš prevzame potomca naslednika, da ga ne izgubi
successor.setRightChild(deleteNode.getRightChild()); // povežemo naslednika z desnim potomcem odstranjene vozlišča
}
return successor;
}
Polna koda metode delete:
public boolean delete(int deleteKey) {
Node current = root;
Node parent = current;
boolean isLeftChild = false; //Depending on whether the node to be deleted is a left or right child of its parent, the boolean variable isLeftChild will be true or false accordingly.
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) { //first case
if (current == root)
current = null;
else if (isLeftChild)
parent.setLeftChild(null);
else
parent.setRightChild(null);
}
else if (current.getRightChild() == null) { //second case
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 { //third case
Node successor = getSuccessor(current);
if (current == root)
root = successor;
else if (isLeftChild)
parent.setLeftChild(successor);
else
parent.setRightChild(successor);
}
return true;
}
The complexity can be approximated to O(log(n)).
Finding the maximum/minimum in a tree
Obviously, to find the minimum/maximum value in the tree, you need to sequentially traverse the chain of left/right tree elements respectively; when you reach a leaf, it will be the minimum/maximum element.
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;
}
The complexity is O(log(n))
Symmetric traversal
Traversal means visiting each node of the tree to perform some action on it.
The algorithm for recursive symmetric traversal:
- Perform an action with the left child
- Perform an action with itself
- Perform an action with the right child
Koda:
public void inOrder(Node current) {
if (current != null) {
inOrder(current.getLeftChild());
System.out.println(current.getData() + " ");//Tu lahko pride vse mogoče
inOrder(current.getRightChild());
}
}
Zaključek
Končno! Če mam kaj nedorečenega ali imate kakšne pripombe, jih pričakujem v komentarjih. Kot sem obljubil, prinašam celoten kodo.
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:
javna klasa BinaryTree<T> {
privatna Node<T> root;
javni Node<T> find(int key) {
Node<T> current = root;
dok (current.getKey() != key) {
ako (key < current.getKey())
current = current.getLeftChild();
inače
current = current.getRightChild();
ako (current == null)
vrati null;
}
vrati current;
}
javna void insert(T insertData, int key) {
Node<T> current = root;
Node<T> parent;
Node<T> newNode = new Node<>(insertData, key);
ako (root == null)
root = newNode;
inače {
dok (true) {
parent = current;
ako (key < current.getKey()) {
current = current.getLeftChild();
ako (current == null) {
parent.setLeftChild(newNode);
vrati;
}
}
inače {
current = current.getRightChild();
ako (current == null) {
parent.setRightChild(newNode);
vrati;
}
}
}
}
}
javni Node<T> getMinimum(Node<T> startPoint) {
Node<T> current = startPoint;
Node<T> parent = current;
dok (current != null) {
parent = current;
current = current.getLeftChild();
}
vrati parent;
}
javni Node<T> getMaximum(Node<T> startPoint) {
Node<T> current = startPoint;
Node<T> parent = current;
dok (current != null) {
parent = current;
current = current.getRightChild();
}
vrati parent;
}
javni Node<T> getSuccessor(Node<T> deleteNode) {
Node<T> parentSuccessor = deleteNode;
Node<T> successor = deleteNode;
Node<T> current = successor.getRightChild();
dok (current != null) {
parentSuccessor = successor;
successor = current;
current = current.getLeftChild();
}
ako (successor != deleteNode.getRightChild()) {
parentSuccessor.setLeftChild(successor.getRightChild());
successor.setRightChild(deleteNode.getRightChild());
}
vrati successor;
}
javni boolean delete(int deleteKey) {
Node<T> current = root;
Node<T> parent = current;
boolean isLeftChild = false;
dok (current.getKey() != deleteKey) {
parent = current;
ako (deleteKey < current.getKey()) {
current = current.getLeftChild();
isLeftChild = true;
} else {
isLeftChild = false;
current = current.getRightChild();
}
ako (current == null)
vrati false;
}
ako (current.getLeftChild() == null && current.getRightChild() == null) {
ako (current == root)
current = null;
inače ako (isLeftChild)
parent.setLeftChild(null);
inače
parent.setRightChild(null);
}
inače ako (current.getRightChild() == null) {
ako (current == root)
root = current.getLeftChild();
inače ako (isLeftChild)
parent.setLeftChild(current.getLeftChild());
inače
current.setRightChild(current.getLeftChild());
} inače ako (current.getLeftChild() == null) {
ako (current == root)
root = current.getRightChild();
inače ako (isLeftChild)
parent.setLeftChild(current.getRightChild());
inače
parent.setRightChild(current.getRightChild());
}
inače {
Node<T> successor = getSuccessor(current);
ako (current == root)
root = successor;
inače ako (isLeftChild)
parent.setLeftChild(successor);
inače
parent.setRightChild(successor);
}
vrati true;
}
javna void inOrder(Node<T> current) {
ako (current != null) {
inOrder(current.getLeftChild());
System.out.println(current.getData() + " ");
inOrder(current.getRightChild());
}
}
}
P.S.
Začetenje do O(n)
Mnogi od vas su mogli primetiti: a šta ako učinite da drvo postane neusaglašeno? Na primer, umetnite čvorove sa rastućim ključevima: 1,2,3,4,5,6… Tada će drvo više ličiti na povezanu listu. I da, drvo će izgubiti svoju hijerarhijsku strukturu, a samim tim i efikasnost pristupa podacima. Složenost operacija pretrage, umetanja, brisanja postaće kao kod povezane liste: O(n). U tome se otkriva jedan od najvažnijih, po mom mišljenju, nedostataka binarnih stabala.
Samo registrovani korisnici mogu učestvovati u anketi. , molim vas.
Nisam dugo na habru, i voleo bih da znam koje teme biste želeli da vidite više?
Podatkovne strukture
Algoritmi (DP, rekurzija, kompresija podataka itd.)
Primena podatkovnih struktura i algoritama u stvarnom životu
Programiranje android aplikacija na Javi
Programiranje web aplikacija na Javi
2 pengguna memberikan suara. 1 pengguna memilih tidak memihak.
Izvor: habr.com
