Prozkoumejte základy binárních vyhledávacích stromů (BST) a naučte se je efektivně implementovat v JavaScriptu. Tento průvodce pokrývá strukturu BST, operace a praktické příklady pro vývojáře po celém světě.
Binární vyhledávací stromy: Komplexní průvodce implementací v JavaScriptu
Binární vyhledávací stromy (BST) jsou základní datovou strukturou v informatice, široce používanou pro efektivní vyhledávání, třídění a získávání dat. Jejich hierarchická struktura umožňuje logaritmickou časovou složitost u mnoha operací, což z nich činí mocný nástroj pro správu velkých datových sad. Tento průvodce poskytuje komplexní přehled BST a demonstruje jejich implementaci v JavaScriptu pro vývojáře po celém světě.
Porozumění binárním vyhledávacím stromům
Co je to binární vyhledávací strom?
Binární vyhledávací strom je stromová datová struktura, kde každý uzel má nanejvýš dvě děti, označované jako levé dítě a pravé dítě. Klíčovou vlastností BST je, že pro jakýkoli daný uzel platí:
- Všechny uzly v levém podstromu mají klíče menší než klíč daného uzlu.
- Všechny uzly v pravém podstromu mají klíče větší než klíč daného uzlu.
Tato vlastnost zajišťuje, že prvky v BST jsou vždy uspořádané, což umožňuje efektivní vyhledávání a získávání dat.
Klíčové pojmy
- Uzel: Základní jednotka stromu, obsahující klíč (data) a ukazatele na levé a pravé dítě.
- Kořen: Nejvyšší uzel ve stromu.
- List: Uzel bez dětí.
- Podstrom: Část stromu s kořenem v určitém uzlu.
- Výška: Délka nejdelší cesty od kořene k listu.
- Hloubka: Délka cesty od kořene k určitému uzlu.
Implementace binárního vyhledávacího stromu v JavaScriptu
Definice třídy Node
Nejprve definujeme třídu `Node`, která bude reprezentovat každý uzel v BST. Každý uzel bude obsahovat `key` pro uložení dat a `left` a `right` ukazatele na své děti.
class Node {
constructor(key) {
this.key = key;
this.left = null;
this.right = null;
}
}
Definice třídy BinarySearchTree
Dále definujeme třídu `BinarySearchTree`. Tato třída bude obsahovat kořenový uzel a metody pro vkládání, vyhledávání, mazání a procházení stromu.
class BinarySearchTree {
constructor() {
this.root = null;
}
// Sem budou přidány metody
}
Vkládání
Metoda `insert` přidává do BST nový uzel s daným klíčem. Proces vkládání zachovává vlastnost BST tím, že umístí nový uzel na příslušnou pozici vzhledem k existujícím uzlům.
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);
}
}
}
Příklad: Vkládání hodnot do 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);
Vyhledávání
Metoda `search` kontroluje, zda v BST existuje uzel s daným klíčem. Prochází strom, porovnává klíč s klíčem aktuálního uzlu a podle toho se přesouvá do levého nebo pravého podstromu.
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;
}
}
Příklad: Vyhledávání hodnoty v BST
console.log(bst.search(9)); // Výstup: true
console.log(bst.search(2)); // Výstup: false
Mazání
Metoda `remove` maže z BST uzel s daným klíčem. Jedná se o nejsložitější operaci, protože při odstraňování uzlu je třeba zachovat vlastnost BST. Je třeba zvážit tři případy:
- Případ 1: Uzel, který má být smazán, je list. Jednoduše ho odstraňte.
- Případ 2: Uzel, který má být smazán, má jedno dítě. Nahraďte uzel jeho dítětem.
- Případ 3: Uzel, který má být smazán, má dvě děti. Najděte následníka v in-order pořadí (nejmenší uzel v pravém podstromu), nahraďte uzel následníkem a poté následníka smažte.
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 {
// klíč je roven klíči uzlu
// případ 1 - listový uzel
if (node.left === null && node.right === null) {
node = null;
return node;
}
// případ 2 - uzel má pouze 1 dítě
if (node.left === null) {
node = node.right;
return node;
} else if (node.right === null) {
node = node.left;
return node;
}
// případ 3 - uzel má 2 děti
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;
}
Příklad: Odstranění hodnoty z BST
bst.remove(7);
console.log(bst.search(7)); // Výstup: false
Procházení stromu
Procházení stromu zahrnuje návštěvu každého uzlu ve stromu v určitém pořadí. Existuje několik běžných metod procházení:
- In-order: Navštíví levý podstrom, poté uzel, poté pravý podstrom. Výsledkem je návštěva uzlů ve vzestupném pořadí.
- Pre-order: Navštíví uzel, poté levý podstrom, poté pravý podstrom.
- Post-order: Navštíví levý podstrom, poté pravý podstrom, poté uzel.
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);
}
}
Příklad: Procházení BST
const printNode = (value) => console.log(value);
bst.inOrderTraverse(printNode); // Výstup: 3 5 8 9 10 11 12 13 14 15 18 20 25
bst.preOrderTraverse(printNode); // Výstup: 11 5 3 9 8 10 15 13 12 14 20 18 25
bst.postOrderTraverse(printNode); // Výstup: 3 8 10 9 12 14 13 18 25 20 15 11
Minimální a maximální hodnoty
Nalezení minimální a maximální hodnoty v BST je díky jeho uspořádané povaze přímočaré.
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;
}
Příklad: Nalezení minimální a maximální hodnoty
console.log(bst.min().key); // Výstup: 3
console.log(bst.max().key); // Výstup: 25
Praktické aplikace binárních vyhledávacích stromů
Binární vyhledávací stromy se používají v různých aplikacích, včetně:
- Databáze: Indexování a vyhledávání dat. Například mnoho databázových systémů používá varianty BST, jako jsou B-stromy, k efektivnímu vyhledávání záznamů. Zvažte globální měřítko databází používaných nadnárodními korporacemi; efektivní získávání dat je prvořadé.
- Překladače: Tabulky symbolů, které ukládají informace o proměnných a funkcích.
- Operační systémy: Plánování procesů a správa paměti.
- Vyhledávače: Indexování webových stránek a řazení výsledků vyhledávání.
- Souborové systémy: Organizace a přístup k souborům. Představte si souborový systém na serveru používaném globálně pro hostování webových stránek; dobře organizovaná struktura založená na BST pomáhá rychlému doručování obsahu.
Úvahy o výkonu
Výkon BST závisí na jeho struktuře. V nejlepším případě, vyvážený BST umožňuje logaritmickou časovou složitost pro operace vkládání, vyhledávání a mazání. V nejhorším případě (např. u zešikmeného stromu) se však časová složitost může zhoršit na lineární čas.
Vyvážené vs. nevyvážené stromy
Vyvážený BST je takový, kde se výška levého a pravého podstromu každého uzlu liší nanejvýš o jedna. Samovyvažovací algoritmy, jako jsou AVL stromy a červeno-černé stromy, zajišťují, že strom zůstane vyvážený, což poskytuje konzistentní výkon. Různé regiony mohou vyžadovat různé úrovně optimalizace na základě zatížení serveru; vyvažování pomáhá udržovat výkon při vysokém globálním využití.
Časová složitost
- Vkládání: O(log n) v průměru, O(n) v nejhorším případě.
- Vyhledávání: O(log n) v průměru, O(n) v nejhorším případě.
- Mazání: O(log n) v průměru, O(n) v nejhorším případě.
- Procházení: O(n), kde n je počet uzlů ve stromu.
Pokročilé koncepty BST
Samovyvažovací stromy
Samovyvažovací stromy jsou BST, které automaticky upravují svou strukturu, aby udržely rovnováhu. To zajišťuje, že výška stromu zůstane logaritmická, což poskytuje konzistentní výkon pro všechny operace. Mezi běžné samovyvažovací stromy patří AVL stromy a červeno-černé stromy.
AVL stromy
AVL stromy udržují rovnováhu tím, že zajišťují, aby se rozdíl výšek mezi levým a pravým podstromem libovolného uzlu lišil nanejvýš o jedna. Když je tato rovnováha narušena, provádějí se rotace k jejímu obnovení.
Červeno-černé stromy
Červeno-černé stromy používají vlastnosti barev (červená nebo černá) k udržení rovnováhy. Jsou složitější než AVL stromy, ale v určitých scénářích nabízejí lepší výkon.
Příklad kódu v JavaScriptu: Kompletní implementace binárního vyhledávacího stromu
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 {
// klíč je roven klíči uzlu
// případ 1 - listový uzel
if (node.left === null && node.right === null) {
node = null;
return node;
}
// případ 2 - uzel má pouze 1 dítě
if (node.left === null) {
node = node.right;
return node;
} else if (node.right === null) {
node = node.left;
return node;
}
// případ 3 - uzel má 2 děti
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);
}
}
}
// Příklad použití
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("Průchod in-order:");
bst.inOrderTraverse(printNode);
console.log("Průchod pre-order:");
bst.preOrderTraverse(printNode);
console.log("Průchod post-order:");
bst.postOrderTraverse(printNode);
console.log("Minimální hodnota:", bst.min().key);
console.log("Maximální hodnota:", bst.max().key);
console.log("Hledat 9:", bst.search(9));
console.log("Hledat 2:", bst.search(2));
bst.remove(7);
console.log("Hledat 7 po odstranění:", bst.search(7));
Závěr
Binární vyhledávací stromy jsou mocnou a všestrannou datovou strukturou s mnoha aplikacemi. Tento průvodce poskytl komplexní přehled BST, pokrývající jejich strukturu, operace a implementaci v JavaScriptu. Porozuměním principům a technikám diskutovaným v tomto průvodci mohou vývojáři po celém světě efektivně využívat BST k řešení široké škály problémů ve vývoji softwaru. Od správy globálních databází po optimalizaci vyhledávacích algoritmů jsou znalosti BST neocenitelným přínosem pro každého programátora.
Jak budete pokračovat ve své cestě v informatice, zkoumání pokročilých konceptů, jako jsou samovyvažovací stromy a jejich různé implementace, dále prohloubí vaše porozumění a schopnosti. Pokračujte v procvičování a experimentování s různými scénáři, abyste si osvojili umění efektivního používání binárních vyhledávacích stromů.