Українська

Ознайомтеся з основами двійкових дерев пошуку (BST) та навчіться їх ефективно реалізовувати на JavaScript. Цей посібник охоплює структуру BST, операції та практичні приклади для розробників з усього світу.

Двійкові дерева пошуку: Повний посібник з реалізації на 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;
  }
}

Визначення класу BinarySearchTree

Далі ми визначаємо клас `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("Симетричний обхід (in-order):");
bst.inOrderTraverse(printNode);

console.log("Прямий обхід (pre-order):");
bst.preOrderTraverse(printNode);

console.log("Зворотний обхід (post-order):");
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 є безцінним активом для будь-якого програміста.

Продовжуючи свою подорож у комп'ютерних науках, дослідження просунутих концепцій, таких як самобалансуючі дерева та їхні різноманітні реалізації, ще більше поглибить ваше розуміння та можливості. Продовжуйте практикуватися та експериментувати з різними сценаріями, щоб оволодіти мистецтвом ефективного використання двійкових дерев пошуку.