Preludë
Ky artikull i kushtohet pemëve binarë të kërkimit. Së fundmi kam shkruar një artikull mbi Aty nuk u përqendrova shumë te pemët binarë, sepse metodat e kërkimit, injektimit, dhe fshirjes nuk ishin të rëndësishme. Tani vendosa të shkruaj një artikull pikërisht mbi pemët. Le të fillojmë.
Një pemë është një strukturë të dhënash, e përbërë nga nyje të lidhura me mola. Mund të thuhet se një pemë është një rast special i grafit. Ja një shembull i një peme:

Kjo nuk është një pemë binare e kërkimit! Të gjitha në kat!
Terminologjia
Rrënja
Rrënja e pemës është nyja e saj më e lartë. Në shembullin e dhënë, kjo është nyja A. Në pemë, nga rrënja në çdo nyjë tjetër mund të çojë vetëm një rrugë! Në të vërtetë, çdo nyjë mund të konsiderohet si rrënja e nënpemës që i përket asaj nyje.
Prindërit/çfarëdo
Të gjitha nyjat, përveç atij rrënjë, kanë saktësisht një mola që çon lart te një nyjë tjetër. Nyja e vendosur lart se aktualja quhet prindi i kësaj nyje. Nyja e vendosur poshtë aktuales dhe e lidhur me të quhet çfarëdo i kësaj nyje. Le të shohim me një shembull. Të marrim nyjën B, atëherë prindi i saj do të jetë nyja A, dhe çfarëdo do të jenë nyjat D, E, dhe F.
Fletë
Një nyjë që nuk ka çfarëdo do të quhet gjethe e pemës. Në shembullin e dhënë, gjethet do të jenë nyjat D, E, F, G, I, J, K.
Kjo është terminologjia kryesore. Konceptet e tjera do të shqyrtohen më tej. Pra, një pemë binar është një pemë në të cilën çdo nyjë ka jo më shumë se dy pasardhës. Siç e keni kuptuar, pema nga shembulli nuk do të jetë binare, pasi nyjat B dhe H kanë më shumë se dy pasardhës. Ja një shembull i një peme binare:

Në nyzat e pemës mund të ketë çdo informacion. Një pemë binare e kërkimit është një pemë binare, për të cilën karakterizohen këto prona:
- TĂ« dy nĂ«npemĂ«t â tĂ« majtĂ« dhe tĂ« djathtĂ« â janĂ« pemĂ« binarĂ« kĂ«rkimesh.
- Të gjitha nyjat e nënpemës së majtë të ndonjë nyje X kanë vlera të çelësit më të vogla se vlera e çelësit të nyjës vetë X.
- Të gjitha nyjat e nënpemës së djathtë të ndonjë nyje X kanë vlera të çelësit më të madhe ose të barabartë me vlerën e çelësit të nyjës vetë X.
ĂelĂ«si â ndonjĂ« karakteristikĂ« e nyjĂ«s (p.sh., numri). ĂelĂ«si Ă«shtĂ« i nevojshĂ«m pĂ«r t'i dhĂ«nĂ« mundĂ«sinĂ« tĂ« gjeni elementin nĂ« pemĂ« qĂ« i pĂ«rket atij çelĂ«si. Ja njĂ« shembull i njĂ« peme binare kĂ«rkimi:

Paraqitja e pemës
Në vazhdim, do të jap disa (ndoshta, të pjesshme) copa kodi për të përmirësuar kuptimin tuaj. Kodi i plotë do të jetë në fund të artikullit.
Pema përbëhet nga nodet. Struktura e nodit:
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;
}
//...metodat e tjera të nodit
}
Ădo nod ka dy pasardhĂ«s (Ă«shtĂ« e mundur, qĂ« pasardhĂ«sit leftChild dhe/ose rightChild do tĂ« pĂ«rmbajnĂ« vlerĂ«n null). Ju, ndoshta, keni kuptuar se nĂ« kĂ«tĂ« rast numri data â janĂ« tĂ« dhĂ«nat e ruajtura nĂ« nod; key â Ă«shtĂ« çelĂ«si i nodit.
Tani që e kuptuam nodin, le të flasim për problemet aktuale me pemët. Këtu dhe më tej, me fjalën "pemë" do të kuptoj konceptin e pemës binare të kërkimit. Struktura e pemës binare:
public class BinaryTree<T> {
private Node<T> root;
//metodat e pemës
}
Si fushë klase, do të na nevojitet vetëm rrënja e pemës, pasi nga rrënja me ndihmën e metodave getLeftChild() dhe getRightChild() mund të arrijmë në çdo nod të pemës.
Algoritmet në pemë
Kërkimi
Supozoni se keni njĂ« pemĂ« tĂ« ndĂ«rtuar. Si tĂ« gjeni elementin me çelĂ«sin key? Duhet tĂ« lĂ«vizni gradualisht nga rrĂ«nja poshtĂ« nĂ«pĂ«r pemĂ« dhe tĂ« krahasoni vlerĂ«n e key me çelĂ«sin e nodit tĂ« radhĂ«s: nĂ«se key Ă«shtĂ« mĂ« i vogĂ«l se çelĂ«si i nodit tĂ« radhĂ«s, atĂ«herĂ« kaloni tek pasardhĂ«si i majtĂ« i nodit, nĂ«se mĂ« i madh â tek i djathti, nĂ«se çelĂ«sat janĂ« tĂ« barabartĂ« â nodi i kĂ«rkuar Ă«shtĂ« gjetur! Kodi pĂ«rkatĂ«s:
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;
}
NĂ«se current bĂ«het null, atĂ«herĂ« pĂ«rfundimi arriti fundit tĂ« pemĂ«s (nĂ« nivelin konceptual, ndodheni nĂ« njĂ« vend joekzistues tĂ« pemĂ«s â pasardhĂ«si i njĂ« gjethe).
Le të shqyrtojmë efikasitetin e algoritmit të kërkimit në një pemë të balancuar (pemë në të cilën nodet janë shpërndarë më shumë ose më pak në mënyrë të barabartë). Atëherë efikasiteti i kërkimit do të jetë O(log(n)), ku logaritet me bazën 2. Shikoni: nëse në një pemë të balancuar ka n elemente, atëherë kjo do të thotë se do të ketë log(n) me bazën 2 nivele të pemës. Dhe në kërkim, për një hap të ciklit, zbrisni një nivel.
Inserting
NĂ«se e keni kuptuar thelbin e kĂ«rkimit, atĂ«herĂ« nuk do t'ju duket e vĂ«shtirĂ« tĂ« kuptoni futjen. Thjesht duhet tĂ« zhyteni nĂ« fletĂ«n e pemĂ«s (sipas rregullave tĂ« zbritjes, pĂ«rshkruara nĂ« kĂ«rkim) dhe tĂ« bĂ«heni pasardhĂ«si i saj â tĂ« majtĂ« ose tĂ« djathtĂ«, nĂ« varĂ«si tĂ« çelĂ«sit. Realizimi:
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;
}
}
}
}
}
Në këtë rast, gjithashtu duhet të ruajmë informacionin për prindin e nodit aktual. Kur current të bëhet null, në variablën parent do të jetë fletë e nevojshme.
Krahasimi i efikasitetit tĂ« futjes, sigurisht, do tĂ« jetĂ« i njĂ«jtĂ« si ai i kĂ«rkimit â O(log(n)).
Ăinstalim
Fshirja â operacioni mĂ« i komplikuar qĂ« do tĂ« duhet tĂ« kryhet me pemĂ«n. ĂshtĂ« e qartĂ« se sĂ« pari duhet tĂ« gjejmĂ« elementin qĂ« do tĂ« fshijmĂ«. Por çfarĂ« pastaj? NĂ«se thjesht i japim referencĂ«s vlerĂ«n null, atĂ«herĂ« do tĂ« humbasim informacionin mbi nĂ«npemĂ«n, rrĂ«nja e sĂ« cilĂ«s Ă«shtĂ« ky nod. Metodat e fshirjes tĂ« pemĂ«s ndahen nĂ« tre raste.
Rasti i parë. Nodi i fshirë nuk ka pasardhës.
Nëse nodi i fshirë nuk ka pasardhës, atëherë kjo do të thotë se ai është një fletë. Prandaj, thjesht mund t'i japim fushave leftChild ose rightChild të prindit të tij vlerën null.
Rasti i dytë. Nodi i fshirë ka një pasardhës.
Ky rast gjithashtu nuk Ă«shtĂ« shumĂ« i komplikuar. Le tĂ« kthehemi nĂ« shembullin tonĂ«. Supozoni se duhet tĂ« fshini elementin me çelĂ«s 14. Pajtohuni se qoftĂ« se ai Ă«shtĂ« pasardhĂ«si i djathtĂ« i nodit me çelĂ«s 10, çdo pasardhĂ«s i tij (nĂ« kĂ«tĂ« rast i djathtĂ«) do tĂ« ketĂ« njĂ« çelĂ«s mĂ« tĂ« madh se 10, prandaj mund ta "prerĂ«" lehtĂ«sisht nga pema, dhe prindin ta lidhim direkt me pasardhĂ«sin e nodit tĂ« fshirĂ«, dmth, nodi me çelĂ«s 10 ta lidhim me nodin 13. Situata do tĂ« ishte e ngjashme nĂ«se do tĂ« duhej tĂ« fshinim njĂ« nod qĂ« Ă«shtĂ« pasardhĂ«s i majtĂ« i prindit tĂ« tij. Mendoni pĂ«r kĂ«tĂ« vetĂ« â Ă«shtĂ« njĂ« analogji e saktĂ«.
Rasti i tretë. Nodi ka dy pasardhës.
Rasti më kompleks. Do ta shqyrtojmë në një shembull të ri.

Kërkimi i pasardhësit.
Supozoni se duhet të fshihet një nod me çelës 25. Kush do ta zëvendësojë atë? Disa nga pasuesit e tij (pasardhësit ose pasardhësit e pasardhësve) duhet të bëhen pasardhës(ai që do të marrë vendin e nodit të fshirë).
Si të kuptojmë, kush duhet të bëhet pasardhës? Në mënyrë intuitore, është e qartë se ky nod në pemë, çelësi i të cilit është i pari më i madh se nodi i fshirë. Algoritmi përfshin të kaluar te pasardhësi i tij të djathtë (gjithmonë te i djathti, sepse u tha më parë se çelësi i pasardhësit është më i madh se çelësi i nodit të fshirë), dhe pastaj të kalojmë përmes zinxhirit të pasardhësve të majtë të këtij pasardhësi të djathtë. Në shembullin tonë, ne duhet të kalojmë te nodi me çelës 35, dhe pastaj të zbresim në mënyrë të vijueshme përmes zinxhirit të pasardhësve të majtë - në këtë rast, ky zinxhir përbëhet vetëm nga nodi me çelës 30. Teknikisht, ne po kërkojmë nodin më të vogël në grupin e nodave që janë më të mëdhenj se nodi që po kërkojmë.

Kodi i metodës për gjetjen e pasardhësit:
public Node<T> getSuccessor(Node<T> deleteNode) {
Node<T> parentSuccessor = deleteNode; // prindi i pasardhësit
Node<T> successor = deleteNode; // pasardhësi
Node<T> current = successor.getRightChild(); // thjesht një nod "i kalueshëm"
while (current != null) {
parentSuccessor = successor;
successor = current;
current = current.getLeftChild();
}
// në daljen nga cikli kemi pasardhësin dhe prindin e pasardhësit
if (successor != deleteNode.getRightChild()) { // nëse pasardhësi nuk përputhet me pasardhësin e djathtë të nodit të fshirë
parentSuccessor.setLeftChild(successor.getRightChild()); // atëherë prindi i tij merr pasardhësin për të mos e humbur atë
successor.setRightChild(deleteNode.getRightChild()); // lidhim pasardhësin me pasardhësin e djathtë të nodit të fshirë
}
return successor;
}
Kodi i plotë i metodës fshi:
public boolean delete(int deleteKey) {
Node current = root;
Node parent = current;
boolean isLeftChild = false; // Në varësi të asaj nëse nodi i fshirë është fëmija i majtë apo i djathtë i prindit të tij, variabli boolean isLeftChild do të marrë vlerën true ose false përkatësisht.
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) { // rast i parë
if (current == root)
current = null;
else if (isLeftChild)
parent.setLeftChild(null);
else
parent.setRightChild(null);
}
else if (current.getRightChild() == null) { // rast i dytë
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 { // rast i tretë
Node successor = getSuccessor(current);
if (current == root)
root = successor;
else if (isLeftChild)
parent.setLeftChild(successor);
else
parent.setRightChild(successor);
}
return true;
}
Kompleksiteti mund të aproksimohet me O(log(n)).
Kërkimi i maksimumit/minimumit në pemë
E qartĂ« se si tĂ« gjeni vlerĂ«n minimale/maksimale nĂ« pemĂ« â duhet tĂ« kaloni nĂ« mĂ«nyrĂ« tĂ« renditur pĂ«rmes zinxhirit tĂ« elemntĂ«ve tĂ« majtĂ«/djathtĂ« tĂ« pemĂ«s pĂ«rkatĂ«sisht; kur tĂ« arrini nĂ« gjethe, ajo do tĂ« jetĂ« elementi minimal/maksimal.
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;
}
Kompleksiteti â O(log(n))
Përshkimi simetrik
PĂ«rshkimi â vizitimi i çdo nodi tĂ« pemĂ«s me qĂ«llim pĂ«r tĂ« bĂ«rĂ« njĂ« veprim me tĂ«.
Algoritmi i përshkimit simetrik recursiv:
- Bëni veprimin me fëmijën e majtë
- Bëni veprimin me vetë
- Bëni veprimin me fëmijën e djathtë
Kodi:
public void inOrder(Node current) {
if (current != null) {
inOrder(current.getLeftChild());
System.out.println(current.getData() + " ");//Here can be anything you want
inOrder(current.getRightChild());
}
}
Përfundim
Finally! If I missed something or you have any comments, I look forward to them in the comments. As promised, here is the full 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:
publik klasa BinaryTree<T> {
private Node<T> root;
publik Node<T> find(int kyç) {
Node<T> current = root;
while (current.getKey() != kyç) {
if (kyç < current.getKey())
current = current.getLeftChild();
else
current = current.getRightChild();
if (current == null)
return null;
}
return current;
}
publik void insert(T insertData, int kyç) {
Node<T> current = root;
Node<T> parent;
Node<T> newNode = new Node<>(insertData, kyç);
if (root == null)
root = newNode;
else {
while (true) {
parent = current;
if (kyç < current.getKey()) {
current = current.getLeftChild();
if (current == null) {
parent.setLeftChild(newNode);
return;
}
}
else {
current = current.getRightChild();
if (current == null) {
parent.setRightChild(newNode);
return;
}
}
}
}
}
publik Node<T> getMinimum(Node<T> startPoint) {
Node<T> current = startPoint;
Node<T> parent = current;
while (current != null) {
parent = current;
current = current.getLeftChild();
}
return parent;
}
publik Node<T> getMaximum(Node<T> startPoint) {
Node<T> current = startPoint;
Node<T> parent = current;
while (current != null) {
parent = current;
current = current.getRightChild();
}
return parent;
}
publik Node<T> getSuccessor(Node<T> deleteNode) {
Node<T> parentSuccessor = deleteNode;
Node<T> successor = deleteNode;
Node<T> 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;
}
publik boolean delete(int deleteKey) {
Node<T> current = root;
Node<T> 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<T> successor = getSuccessor(current);
if (current == root)
root = successor;
else if (isLeftChild)
parent.setLeftChild(successor);
else
parent.setRightChild(successor);
}
return true;
}
publik void inOrder(Node<T> current) {
if (current != null) {
inOrder(current.getLeftChild());
System.out.println(current.getData() + " ");
inOrder(current.getRightChild());
}
}
}
P.S.
Degenerimi në O(n)
Shumica e jush mund të ketë vënë re: çfarë ndodh nëse e bëjmë pemën të paekuilibruar? Për shembull, nëse vendosim në pemë nodet me çelësa në rritje: 1,2,3,4,5,6... Atëherë, pema do të ngjajë më shumë si një listë e lidhur. Dhe po, pema do të humbasë strukturën e saj si pemë, duke humbur kështu edhe efikasitetin e qasjes në të dhëna. Kompleksiteti i operacioneve të kërkimit, shtimit, fshirjes do të bëhet si ai i një liste të lidhur: O(n). Kjo tregon një nga disavantazhet më të rëndësishme, sipas mendimit tim, të pemëve binarë.
Vetëm përdoruesit e regjistruar mund të marrin pjesë në anketë. , ju lutem.
Nuk kam qenë shumë kohë në Habr, dhe do të doja të dija, për cilat tema do të dëshironit të shihni më shumë artikuj?
Struktura të dhënash
Algoritmet (DP, rikthim, kompresim të dhënash etj.)
Zbatimi i strukturave të dhënash dhe algoritmeve në jetën reale
Programimi i aplikacioneve Android në Java
Programimi i aplikacioneve web në Java
Kanë votuar 2 përdorues. 1 përdorues abstenoi.
Burimi: habr.com
