Raziščite osnove binarnih iskalnih dreves (BST) in se naučite, kako jih učinkovito implementirati v JavaScriptu. Ta vodnik pokriva strukturo, operacije in praktične primere za razvijalce po vsem svetu.
Binarna iskalna drevesa: Celovit vodnik za implementacijo v JavaScriptu
Binarna iskalna drevesa (BST) so temeljna podatkovna struktura v računalništvu, ki se pogosto uporablja za učinkovito iskanje, razvrščanje in pridobivanje podatkov. Njihova hierarhična struktura omogoča logaritemsko časovno zahtevnost pri mnogih operacijah, zaradi česar so močno orodje za upravljanje velikih zbirk podatkov. Ta vodnik ponuja celovit pregled BST-jev in prikazuje njihovo implementacijo v JavaScriptu, namenjeno razvijalcem po vsem svetu.
Razumevanje binarnih iskalnih dreves
Kaj je binarno iskalno drevo?
Binarno iskalno drevo je drevesna podatkovna struktura, kjer ima vsako vozlišče največ dva otroka, imenovana levi otrok in desni otrok. Ključna lastnost BST-ja je, da za vsako dano vozlišče velja:
- Vsa vozlišča v levem poddrevesu imajo ključe, manjše od ključa vozlišča.
- Vsa vozlišča v desnem poddrevesu imajo ključe, večje od ključa vozlišča.
Ta lastnost zagotavlja, da so elementi v BST vedno urejeni, kar omogoča učinkovito iskanje in pridobivanje.
Ključni pojmi
- Vozlišče: Osnovna enota v drevesu, ki vsebuje ključ (podatek) in kazalca na levega in desnega otroka.
- Koren: Najvišje vozlišče v drevesu.
- List: Vozlišče brez otrok.
- Poddrevo: Del drevesa s korenom v določenem vozlišču.
- Višina: Dolžina najdaljše poti od korena do lista.
- Globina: Dolžina poti od korena do določenega vozlišča.
Implementacija binarnega iskalnega drevesa v JavaScriptu
Definiranje razreda Node
Najprej definiramo razred `Node`, ki predstavlja vsako vozlišče v BST. Vsako vozlišče bo vsebovalo `key` za shranjevanje podatka ter `left` in `right` kazalca na svoja otroka.
class Node {
constructor(key) {
this.key = key;
this.left = null;
this.right = null;
}
}
Definiranje razreda BinarySearchTree
Nato definiramo razred `BinarySearchTree`. Ta razred bo vseboval korensko vozlišče in metode za vstavljanje, iskanje, brisanje in prehajanje po drevesu.
class BinarySearchTree {
constructor() {
this.root = null;
}
// Metode bodo dodane tukaj
}
Vstavljanje
Metoda `insert` doda novo vozlišče z danim ključem v BST. Postopek vstavljanja ohranja lastnost BST z umestitvijo novega vozlišča na ustrezno mesto glede na obstoječa vozlišča.
insert(key) {
const newNode = new Node(key);
if (this.root === null) {
this.root = newNode;
} else {
this.insertNode(this.root, newNode);
}
}
insertNode(node, newNode) {
if (newNode.key < node.key) {
if (node.left === null) {
node.left = newNode;
} else {
this.insertNode(node.left, newNode);
}
} else {
if (node.right === null) {
node.right = newNode;
} else {
this.insertNode(node.right, newNode);
}
}
}
Primer: Vstavljanje vrednosti v BST
const bst = new BinarySearchTree();
bst.insert(11);
bst.insert(7);
bst.insert(15);
bst.insert(5);
bst.insert(3);
bst.insert(9);
bst.insert(8);
bst.insert(10);
bst.insert(13);
bst.insert(12);
bst.insert(14);
bst.insert(20);
bst.insert(18);
bst.insert(25);
Iskanje
Metoda `search` preveri, ali v BST obstaja vozlišče z danim ključem. Prehaja po drevesu, primerja ključ s ključem trenutnega vozlišča in se ustrezno premika v levo ali desno poddrevo.
search(key) {
return this.searchNode(this.root, key);
}
searchNode(node, key) {
if (node === null) {
return false;
}
if (key < node.key) {
return this.searchNode(node.left, key);
} else if (key > node.key) {
return this.searchNode(node.right, key);
} else {
return true;
}
}
Primer: Iskanje vrednosti v BST
console.log(bst.search(9)); // Output: true
console.log(bst.search(2)); // Output: false
Brisanje
Metoda `remove` izbriše vozlišče z danim ključem iz BST. To je najbolj zapletena operacija, saj mora med odstranjevanjem vozlišča ohraniti lastnost BST. Upoštevati je treba tri primere:
- Primer 1: Vozlišče, ki ga želimo izbrisati, je list. Preprosto ga odstranimo.
- Primer 2: Vozlišče, ki ga želimo izbrisati, ima enega otroka. Vozlišče nadomestimo z njegovim otrokom.
- Primer 3: Vozlišče, ki ga želimo izbrisati, ima dva otroka. Poiščemo neposrednega naslednika (najmanjše vozlišče v desnem poddrevesu), zamenjamo vozlišče z naslednikom in nato izbrišemo naslednika.
remove(key) {
this.root = this.removeNode(this.root, key);
}
removeNode(node, key) {
if (node === null) {
return null;
}
if (key < node.key) {
node.left = this.removeNode(node.left, key);
return node;
} else if (key > node.key) {
node.right = this.removeNode(node.right, key);
return node;
} else {
// ključ je enak ključu vozlišča
// primer 1 - list
if (node.left === null && node.right === null) {
node = null;
return node;
}
// primer 2 - vozlišče ima samo 1 otroka
if (node.left === null) {
node = node.right;
return node;
} else if (node.right === null) {
node = node.left;
return node;
}
// primer 3 - vozlišče ima 2 otroka
const aux = this.findMinNode(node.right);
node.key = aux.key;
node.right = this.removeNode(node.right, aux.key);
return node;
}
}
findMinNode(node) {
let current = node;
while (current != null && current.left != null) {
current = current.left;
}
return current;
}
Primer: Odstranjevanje vrednosti iz BST
bst.remove(7);
console.log(bst.search(7)); // Output: false
Prehajanje drevesa
Prehajanje drevesa vključuje obisk vsakega vozlišča v drevesu v določenem vrstnem redu. Obstaja več običajnih metod prehajanja:
- Prečni (in-order): Obišče levo poddrevo, nato vozlišče, nato desno poddrevo. Posledica tega je obisk vozlišč v naraščajočem vrstnem redu.
- Predponski (pre-order): Obišče vozlišče, nato levo poddrevo, nato desno poddrevo.
- Postponski (post-order): Obišče levo poddrevo, nato desno poddrevo, nato vozlišče.
inOrderTraverse(callback) {
this.inOrderTraverseNode(this.root, callback);
}
inOrderTraverseNode(node, callback) {
if (node !== null) {
this.inOrderTraverseNode(node.left, callback);
callback(node.key);
this.inOrderTraverseNode(node.right, callback);
}
}
preOrderTraverse(callback) {
this.preOrderTraverseNode(this.root, callback);
}
preOrderTraverseNode(node, callback) {
if (node !== null) {
callback(node.key);
this.preOrderTraverseNode(node.left, callback);
this.preOrderTraverseNode(node.right, callback);
}
}
postOrderTraverse(callback) {
this.postOrderTraverseNode(this.root, callback);
}
postOrderTraverseNode(node, callback) {
if (node !== null) {
this.postOrderTraverseNode(node.left, callback);
this.postOrderTraverseNode(node.right, callback);
callback(node.key);
}
}
Primer: Prehajanje po BST
const printNode = (value) => console.log(value);
bst.inOrderTraverse(printNode); // Output: 3 5 8 9 10 11 12 13 14 15 18 20 25
bst.preOrderTraverse(printNode); // Output: 11 5 3 9 8 10 15 13 12 14 20 18 25
bst.postOrderTraverse(printNode); // Output: 3 8 10 9 12 14 13 18 25 20 15 11
Minimalne in maksimalne vrednosti
Iskanje minimalne in maksimalne vrednosti v BST je preprosto zaradi njegove urejene narave.
min() {
return this.minNode(this.root);
}
minNode(node) {
let current = node;
while (current !== null && current.left !== null) {
current = current.left;
}
return current;
}
max() {
return this.maxNode(this.root);
}
maxNode(node) {
let current = node;
while (current !== null && current.right !== null) {
current = current.right;
}
return current;
}
Primer: Iskanje minimalne in maksimalne vrednosti
console.log(bst.min().key); // Output: 3
console.log(bst.max().key); // Output: 25
Praktična uporaba binarnih iskalnih dreves
Binarna iskalna drevesa se uporabljajo v različnih aplikacijah, vključno z:
- Podatkovne baze: Indeksiranje in iskanje podatkov. Na primer, številni sistemi podatkovnih baz uporabljajo različice BST, kot so B-drevesa, za učinkovito lociranje zapisov. Pomislite na globalni obseg podatkovnih baz, ki jih uporabljajo multinacionalne korporacije; učinkovito pridobivanje podatkov je ključnega pomena.
- Prevajalniki: Simbolne tabele, ki shranjujejo informacije o spremenljivkah in funkcijah.
- Operacijski sistemi: Razporejanje procesov in upravljanje pomnilnika.
- Iskalniki: Indeksiranje spletnih strani in razvrščanje rezultatov iskanja.
- Datotečni sistemi: Organiziranje in dostopanje do datotek. Predstavljajte si datotečni sistem na strežniku, ki se globalno uporablja za gostovanje spletnih mest; dobro organizirana struktura, ki temelji na BST, pomaga pri hitrem serviranju vsebine.
Učinkovitost delovanja
Učinkovitost delovanja BST je odvisna od njegove strukture. V najboljšem primeru uravnoteženo BST omogoča logaritemsko časovno zahtevnost za operacije vstavljanja, iskanja in brisanja. Vendar pa se lahko v najslabšem primeru (npr. pri poševnem drevesu) časovna zahtevnost poslabša na linearni čas.
Uravnotežena proti neuravnoteženim drevesom
Uravnoteženo BST je tisto, kjer se višina levega in desnega poddrevesa vsakega vozlišča razlikuje za največ ena. Samouravnoteževalni algoritmi, kot so AVL drevesa in rdeče-črna drevesa, zagotavljajo, da drevo ostane uravnoteženo, kar zagotavlja dosledno delovanje. Različne regije morda zahtevajo različne stopnje optimizacije glede na obremenitev strežnika; uravnoteženje pomaga ohranjati delovanje pri visoki globalni uporabi.
Časovna zahtevnost
- Vstavljanje: Povprečno O(log n), v najslabšem primeru O(n).
- Iskanje: Povprečno O(log n), v najslabšem primeru O(n).
- Brisanje: Povprečno O(log n), v najslabšem primeru O(n).
- Prehajanje: O(n), kjer je n število vozlišč v drevesu.
Napredni koncepti BST
Samouravnoteževalna drevesa
Samouravnoteževalna drevesa so BST-ji, ki samodejno prilagajajo svojo strukturo za ohranjanje ravnotežja. To zagotavlja, da višina drevesa ostane logaritemska, kar zagotavlja dosledno delovanje za vse operacije. Pogosta samouravnoteževalna drevesa vključujejo AVL drevesa in rdeče-črna drevesa.
AVL drevesa
AVL drevesa ohranjajo ravnotežje tako, da zagotavljajo, da je razlika v višini med levim in desnim poddrevesom katerega koli vozlišča največ ena. Ko je to ravnotežje porušeno, se izvedejo rotacije za ponovno vzpostavitev ravnotežja.
Rdeče-črna drevesa
Rdeče-črna drevesa uporabljajo barvne lastnosti (rdeča ali črna) za ohranjanje ravnotežja. So bolj zapletena kot AVL drevesa, vendar v določenih primerih ponujajo boljšo zmogljivost.
Primer kode v JavaScriptu: Popolna implementacija binarnega iskalnega drevesa
class Node {
constructor(key) {
this.key = key;
this.left = null;
this.right = null;
}
}
class BinarySearchTree {
constructor() {
this.root = null;
}
insert(key) {
const newNode = new Node(key);
if (this.root === null) {
this.root = newNode;
} else {
this.insertNode(this.root, newNode);
}
}
insertNode(node, newNode) {
if (newNode.key < node.key) {
if (node.left === null) {
node.left = newNode;
} else {
this.insertNode(node.left, newNode);
}
} else {
if (node.right === null) {
node.right = newNode;
} else {
this.insertNode(node.right, newNode);
}
}
}
search(key) {
return this.searchNode(this.root, key);
}
searchNode(node, key) {
if (node === null) {
return false;
}
if (key < node.key) {
return this.searchNode(node.left, key);
} else if (key > node.key) {
return this.searchNode(node.right, key);
} else {
return true;
}
}
remove(key) {
this.root = this.removeNode(this.root, key);
}
removeNode(node, key) {
if (node === null) {
return null;
}
if (key < node.key) {
node.left = this.removeNode(node.left, key);
return node;
} else if (key > node.key) {
node.right = this.removeNode(node.right, key);
return node;
} else {
// ključ je enak ključu vozlišča
// primer 1 - list
if (node.left === null && node.right === null) {
node = null;
return node;
}
// primer 2 - vozlišče ima samo 1 otroka
if (node.left === null) {
node = node.right;
return node;
} else if (node.right === null) {
node = node.left;
return node;
}
// primer 3 - vozlišče ima 2 otroka
const aux = this.findMinNode(node.right);
node.key = aux.key;
node.right = this.removeNode(node.right, aux.key);
return node;
}
}
findMinNode(node) {
let current = node;
while (current != null && current.left != null) {
current = current.left;
}
return current;
}
min() {
return this.minNode(this.root);
}
minNode(node) {
let current = node;
while (current !== null && current.left !== null) {
current = current.left;
}
return current;
}
max() {
return this.maxNode(this.root);
}
maxNode(node) {
let current = node;
while (current !== null && current.right !== null) {
current = current.right;
}
return current;
}
inOrderTraverse(callback) {
this.inOrderTraverseNode(this.root, callback);
}
inOrderTraverseNode(node, callback) {
if (node !== null) {
this.inOrderTraverseNode(node.left, callback);
callback(node.key);
this.inOrderTraverseNode(node.right, callback);
}
}
preOrderTraverse(callback) {
this.preOrderTraverseNode(this.root, callback);
}
preOrderTraverseNode(node, callback) {
if (node !== null) {
callback(node.key);
this.preOrderTraverseNode(node.left, callback);
this.preOrderTraverseNode(node.right, callback);
}
}
postOrderTraverse(callback) {
this.postOrderTraverseNode(this.root, callback);
}
postOrderTraverseNode(node, callback) {
if (node !== null) {
this.postOrderTraverseNode(node.left, callback);
this.postOrderTraverseNode(node.right, callback);
callback(node.key);
}
}
}
// Primer uporabe
const bst = new BinarySearchTree();
bst.insert(11);
bst.insert(7);
bst.insert(15);
bst.insert(5);
bst.insert(3);
bst.insert(9);
bst.insert(8);
bst.insert(10);
bst.insert(13);
bst.insert(12);
bst.insert(14);
bst.insert(20);
bst.insert(18);
bst.insert(25);
const printNode = (value) => console.log(value);
console.log("Prečni obhod:");
bst.inOrderTraverse(printNode);
console.log("Predponski obhod:");
bst.preOrderTraverse(printNode);
console.log("Postponski obhod:");
bst.postOrderTraverse(printNode);
console.log("Minimalna vrednost:", bst.min().key);
console.log("Maksimalna vrednost:", bst.max().key);
console.log("Iskanje 9:", bst.search(9));
console.log("Iskanje 2:", bst.search(2));
bst.remove(7);
console.log("Iskanje 7 po odstranitvi:", bst.search(7));
Zaključek
Binarna iskalna drevesa so močna in vsestranska podatkovna struktura s številnimi aplikacijami. Ta vodnik je ponudil celovit pregled BST-jev, ki zajema njihovo strukturo, operacije in implementacijo v JavaScriptu. Z razumevanjem načel in tehnik, obravnavanih v tem vodniku, lahko razvijalci po vsem svetu učinkovito uporabljajo BST-je za reševanje širokega spektra problemov pri razvoju programske opreme. Od upravljanja globalnih podatkovnih baz do optimizacije iskalnih algoritmov je znanje o BST-jih neprecenljivo sredstvo za vsakega programerja.
Ko boste nadaljevali svojo pot v računalništvu, bo raziskovanje naprednih konceptov, kot so samouravnoteževalna drevesa in njihove različne implementacije, še dodatno izboljšalo vaše razumevanje in zmožnosti. Nadaljujte z vajo in eksperimentiranjem z različnimi scenariji, da boste obvladali umetnost učinkovite uporabe binarnih iskalnih dreves.