Български

Разгледайте основите на двоичните дървета за търсене (BST) и научете как да ги имплементирате ефективно в JavaScript. Ръководството обхваща структура, операции и примери.

Двоични дървета за търсене: Цялостно ръководство за имплементация в JavaScript

Двоичните дървета за търсене (BST) са основна структура от данни в компютърните науки, широко използвана за ефективно търсене, сортиране и извличане на данни. Тяхната йерархична структура позволява логаритмична времева сложност при много операции, което ги прави мощен инструмент за управление на големи набори от данни. Това ръководство предоставя изчерпателен преглед на BST и демонстрира тяхната имплементация в JavaScript, насочено към разработчици от цял свят.

Разбиране на двоичните дървета за търсене

Какво е двоично дърво за търсене?

Двоичното дърво за търсене е дървовидна структура от данни, при която всеки възел има най-много две деца, наричани ляво дете и дясно дете. Ключовото свойство на BST е, че за всеки даден възел:

Това свойство гарантира, че елементите в BST винаги са подредени, което позволява ефективно търсене и извличане.

Ключови понятия

Имплементиране на двоично дърво за търсене в JavaScript

Дефиниране на класа Node

Първо, дефинираме клас `Node`, който да представя всеки възел в BST. Всеки възел ще съдържа `key` за съхранение на данните и указатели `left` и `right` към своите деца.


class Node {
  constructor(key) {
    this.key = key;
    this.left = null;
    this.right = null;
  }
}

Дефиниране на класа Binary Search Tree

След това дефинираме класа `BinarySearchTree`. Този клас ще съдържа коренния възел и методи за вмъкване, търсене, изтриване и обхождане на дървото.


class BinarySearchTree {
  constructor() {
    this.root = null;
  }

  // Методите ще бъдат добавени тук
}

Вмъкване

Методът `insert` добавя нов възел с дадения ключ към BST. Процесът на вмъкване поддържа свойството на BST, като поставя новия възел на подходяща позиция спрямо съществуващите възли.


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);
    }
  }
}

Пример: Вмъкване на стойности в 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);

Търсене

Методът `search` проверява дали в BST съществува възел с дадения ключ. Той обхожда дървото, сравнявайки ключа с ключа на текущия възел и преминавайки съответно към лявото или дясното поддърво.


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;
  }
}

Пример: Търсене на стойност в BST


console.log(bst.search(9));  // Изход: true
console.log(bst.search(2));  // Изход: false

Изтриване

Методът `remove` изтрива възел с дадения ключ от BST. Това е най-сложната операция, тъй като трябва да се запази свойството на BST при премахването на възела. Има три случая, които трябва да се разгледат:


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 {
    // ключът е равен на ключа на възела

    // случай 1 - възел листо
    if (node.left === null && node.right === null) {
      node = null;
      return node;
    }

    // случай 2 - възелът има само едно дете
    if (node.left === null) {
      node = node.right;
      return node;
    } else if (node.right === null) {
      node = node.left;
      return node;
    }

    // случай 3 - възелът има две деца
    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;
}

Пример: Премахване на стойност от BST


bst.remove(7);
console.log(bst.search(7)); // Изход: false

Обхождане на дърво

Обхождането на дърво включва посещаване на всеки възел в дървото в определен ред. Съществуват няколко често срещани метода за обхождане:


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);
  }
}

Пример: Обхождане на BST


const printNode = (value) => console.log(value);

bst.inOrderTraverse(printNode);   // Изход: 3 5 8 9 10 11 12 13 14 15 18 20 25
bst.preOrderTraverse(printNode);  // Изход: 11 5 3 9 8 10 15 13 12 14 20 18 25
bst.postOrderTraverse(printNode); // Изход: 3 8 10 9 12 14 13 18 25 20 15 11

Минимални и максимални стойности

Намирането на минималната и максималната стойност в BST е лесно, благодарение на неговата подредена природа.


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;
}

Пример: Намиране на минимална и максимална стойност


console.log(bst.min().key); // Изход: 3
console.log(bst.max().key); // Изход: 25

Практически приложения на двоичните дървета за търсене

Двоичните дървета за търсене се използват в различни приложения, включително:

Съображения за производителност

Производителността на BST зависи от неговата структура. В най-добрия случай, балансирано BST позволява логаритмична времева сложност за операциите по вмъкване, търсене и изтриване. Въпреки това, в най-лошия случай (напр. изкривено дърво), времевата сложност може да се влоши до линейно време.

Балансирани срещу небалансирани дървета

Балансираното BST е такова, при което височината на лявото и дясното поддърво на всеки възел се различава с най-много едно. Алгоритмите за самобалансиране, като AVL дървета и червено-черни дървета, гарантират, че дървото остава балансирано, осигурявайки постоянна производителност. Различните региони може да изискват различни нива на оптимизация въз основа на натоварването на сървъра; балансирането помага за поддържане на производителността при високо глобално използване.

Времева сложност

Разширени концепции за BST

Самобалансиращи се дървета

Самобалансиращите се дървета са BST, които автоматично коригират структурата си, за да поддържат баланс. Това гарантира, че височината на дървото остава логаритмична, осигурявайки постоянна производителност за всички операции. Често срещани самобалансиращи се дървета включват AVL дървета и червено-черни дървета.

AVL дървета

AVL дърветата поддържат баланс, като гарантират, че разликата във височината между лявото и дясното поддърво на всеки възел е най-много едно. Когато този баланс се наруши, се извършват ротации за възстановяване на баланса.

Червено-черни дървета

Червено-черните дървета използват цветови свойства (червено или черно), за да поддържат баланс. Те са по-сложни от AVL дърветата, но предлагат по-добра производителност в определени сценарии.

Пример с код на JavaScript: Пълна имплементация на двоично дърво за търсене


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 {
      // ключът е равен на ключа на възела

      // случай 1 - възел листо
      if (node.left === null && node.right === null) {
        node = null;
        return node;
      }

      // случай 2 - възелът има само едно дете
      if (node.left === null) {
        node = node.right;
        return node;
      } else if (node.right === null) {
        node = node.left;
        return node;
      }

      // случай 3 - възелът има две деца
      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);
    }
  }
}

// Пример за употреба
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("Центрирано обхождане:");
bst.inOrderTraverse(printNode);

console.log("Пряко обхождане:");
bst.preOrderTraverse(printNode);

console.log("Обратно обхождане:");
bst.postOrderTraverse(printNode);

console.log("Минимална стойност:", bst.min().key);
console.log("Максимална стойност:", bst.max().key);

console.log("Търсене на 9:", bst.search(9));
console.log("Търсене на 2:", bst.search(2));

bst.remove(7);
console.log("Търсене на 7 след премахване:", bst.search(7));

Заключение

Двоичните дървета за търсене са мощна и универсална структура от данни с множество приложения. Това ръководство предостави изчерпателен преглед на BST, обхващайки тяхната структура, операции и имплементация в JavaScript. Като разбират принципите и техниките, обсъдени в това ръководство, разработчиците от цял свят могат ефективно да използват BST за решаване на широк кръг от проблеми в разработката на софтуер. От управление на глобални бази данни до оптимизиране на алгоритми за търсене, познаването на BST е безценен актив за всеки програмист.

Докато продължавате пътуването си в компютърните науки, изследването на напреднали концепции като самобалансиращи се дървета и техните различни имплементации ще подобри още повече вашето разбиране и възможности. Продължавайте да практикувате и експериментирате с различни сценарии, за да овладеете изкуството на ефективното използване на двоични дървета за търсене.