Čeština

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í:

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

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:


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í:


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ě:

Ú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

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ů.