Русский

Изучите основы бинарных деревьев поиска (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;
  }
}

Определение класса 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 {
    // ключ равен node.key

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

    // случай 2 - у узла только 1 потомок
    if (node.left === null) {
      node = node.right;
      return node;
    } else if (node.right === null) {
      node = node.left;
      return node;
    }

    // случай 3 - у узла 2 потомка
    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 — это дерево, в котором высота левого и правого поддеревьев каждого узла отличается не более чем на единицу. Алгоритмы самобалансировки, такие как АВЛ-деревья и красно-чёрные деревья, гарантируют, что дерево остаётся сбалансированным, обеспечивая стабильную производительность. В разных регионах могут потребоваться разные уровни оптимизации в зависимости от нагрузки на сервер; балансировка помогает поддерживать производительность при высокой глобальной нагрузке.

Временная сложность

Продвинутые концепции BST

Самобалансирующиеся деревья

Самобалансирующиеся деревья — это BST, которые автоматически корректируют свою структуру для поддержания баланса. Это гарантирует, что высота дерева остаётся логарифмической, обеспечивая стабильную производительность для всех операций. К распространённым самобалансирующимся деревьям относятся АВЛ-деревья и красно-чёрные деревья.

АВЛ-деревья

АВЛ-деревья поддерживают баланс, гарантируя, что разница в высоте между левым и правым поддеревьями любого узла составляет не более единицы. Когда этот баланс нарушается, выполняются вращения для его восстановления.

Красно-чёрные деревья

Красно-чёрные деревья используют свойства цвета (красный или чёрный) для поддержания баланса. Они сложнее АВЛ-деревьев, но в некоторых сценариях обеспечивают лучшую производительность.

Пример кода на 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 {
      // ключ равен node.key

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

      // случай 2 - у узла только 1 потомок
      if (node.left === null) {
        node = node.right;
        return node;
      } else if (node.right === null) {
        node = node.left;
        return node;
      }

      // случай 3 - у узла 2 потомка
      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 является бесценным активом для любого программиста.

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