Эта статья является частью серии статей о бинарных деревьях поиска, предыдущие части читайте здесь:







Обзор

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

Вставка BST

Вставка BST: вставка нового узла в двоичное дерево поиска включает в себя поиск подходящей позиции для нового ключа и добавление его в качестве конечного узла. Алгоритм обычно следует следующим шагам:

  1. Начните с корневого узла.
  2. Если дерево пусто, создайте новый узел и сделайте его корнем.
  3. Если ключ, который нужно вставить, меньше, чем ключ текущего узла, перейти к левому поддереву.
  4. Если вставляемый ключ больше, чем ключ текущего узла, перейдите к правому поддереву.
  5. Повторяйте шаги 3 и 4, пока не будет найдено подходящее пустое место.
  6. Создайте новый узел с ключом и вставьте его как конечный узел.

Этот процесс гарантирует, что свойство упорядочения BST сохраняется, поскольку ключ каждого узла сравнивается, чтобы определить правильный путь прохождения для вставки. Временная сложность вставки BST составляет в среднем O (log n) для сбалансированного дерева и может ухудшиться до O (n) в худшем случае, если дерево станет несбалансированным.

Поиск BST

Поиск определенного ключа в двоичном дереве поиска следует аналогичному подходу на основе пути, используя свойство упорядочения дерева для уменьшения пространства поиска. Алгоритм следующий:

  1. Начните с корневого узла.
  2. Если дерево пусто или ключ текущего узла совпадает с ключом поиска, вернуть текущий узел.
  3. Если ключ поиска меньше ключа текущего узла, перейти к левому поддереву.
  4. Если ключ поиска больше, чем ключ текущего узла, перейти к правому поддереву.
  5. Повторяйте шаги 3 и 4, пока не будет найден ключ поиска или не будет достигнут конечный узел.

Поиск BST обеспечивает эффективный способ поиска определенного ключа в большом наборе данных со средней временной сложностью O(log n) для сбалансированного дерева. Однако в худшем случае, когда дерево сильно искажено, временная сложность может быть O(n), что делает его похожим на связанный список.

Реализация Python

Узел двоичного дерева поиска

Прежде чем мы приступим к реализации, давайте определим структуру узла BST. Каждый узел состоит из значения/ключа и ссылок на его левый и правый дочерние узлы. Мы можем представить эту структуру в большинстве языков программирования, используя класс или структуру. Вот пример на Python:

class Node:
    def __init__(self, value):
        self.value = value
        self.left = None
        self.right = None

Вставка двоичного дерева поиска

Вставка узла в BST включает обход дерева на основе значения узла и поиск подходящей позиции для сохранения свойства упорядочения. Вот реализация кода для вставки BST:

def insert(root, value):
    if root is None:
        return Node(value)
    
    if value < root.value:
        root.left = insert(root.left, value)
    else:
        root.right = insert(root.right, value)
    
    return root

Функция insert принимает корневой узел и значение в качестве входных данных. Если корень равен None, это означает, что дерево пусто, и мы создаем новый узел с заданным значением в качестве корня. В противном случае мы сравниваем значение со значением корня и рекурсивно вставляем узел в левое поддерево, если оно меньше, или в правое поддерево, если оно больше. Функция возвращает измененный корневой узел после вставки.

Интуиция стека вызовов

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

Рассмотрим следующий BST:

       5
     /   \
    3     8
   / \   / \
  2   4  6  9

Мы хотим вставить новый узел со значением 7 в этот BST.

  1. Мы начинаем процесс вставки, вызывая функцию insert с корневым узлом и значением 7.
  2. Первоначально стек вызовов состоит из одного кадра, представляющего процесс вставки в корневом узле.
  3. Обходя дерево, мы сравниваем значение 7 со значением текущего узла, которое равно 5. Поскольку 7 больше 5, мы движемся к правому поддереву.
  4. Чтобы продолжить процесс вставки, мы рекурсивно вызываем функцию insert с правым дочерним элементом корневого узла (значение 8) и значением 7.
  5. Этот рекурсивный вызов создает новый фрейм поверх стека вызовов, представляющий процесс вставки в правый дочерний элемент корневого узла.
  6. Точно так же мы сравниваем значение 7 со значением текущего узла, которое равно 8. Поскольку 7 меньше 8, мы перемещаемся в левое поддерево.
  7. Мы рекурсивно вызываем функцию insert с левым потомком правого потомка корневого узла (значение 6) и значением 7.
  8. Этот рекурсивный вызов создает еще один фрейм поверх стека вызовов, представляющий процесс вставки в левый дочерний элемент правого дочернего элемента корневого узла.
  9. Теперь мы сравниваем значение 7 со значением текущего узла, которое равно 6. Поскольку 7 больше 6, мы переходим к правому поддереву. Однако, поскольку правильного потомка нет, мы нашли подходящую позицию для нового узла.
  10. Мы создаем новый узел со значением 7 и делаем его правым дочерним элементом текущего узла (значение 6).
  11. Рекурсивный вызов для этого фрейма возвращает измененное поддерево с новым узлом, а фрейм удаляется из стека вызовов.
  12. Рекурсивный вызов предыдущего фрейма (правого дочернего элемента корневого узла) также возвращает измененное поддерево с обновленным правым дочерним элементом, и этот фрейм также удаляется из стека вызовов.
  13. Наконец, первоначальный вызов функции insert в корневом узле возвращает измененный BST с новым узлом, завершая процесс вставки.

Следуя этому процессу, новый узел со значением 7 успешно вставляется в BST.

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

Двоичный поиск по дереву поиска

Поиск определенного значения в BST включает обход дерева путем сравнения значения с узлами и перемещения влево или вправо в зависимости от свойства упорядочения. Вот реализация кода для поиска BST:

def search(root, value):
    if root is None or root.value == value:
        return root
    
    if value < root.value:
        return search(root.left, value)
    else:
        return search(root.right, value)

Функция search принимает корневой узел и значение для поиска. Если корень равен None или значение совпадает со значением корня, мы либо возвращаем корень (если найден), либо None (если не найден). В противном случае мы рекурсивно ищем в левом поддереве, если значение меньше, или в правом поддереве, если оно больше.

Интуиция стека вызовов

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

Рассмотрим следующий BST:

       5
     /   \
    3     8
   / \   / \
  2   4  6  9

Мы хотим найти значение 6 в этом BST.

  1. Мы начинаем процесс поиска, вызывая функцию search с корневым узлом и значением 6.
  2. Первоначально стек вызовов состоит из одного кадра, представляющего процесс поиска в корневом узле.
  3. Обходя дерево, мы сравниваем значение 6 со значением текущего узла, которое равно 5. Поскольку 6 больше 5, мы движемся к правому поддереву.
  4. Чтобы продолжить процесс поиска, мы рекурсивно вызываем функцию search с правым потомком корневого узла (значение 8) и значением 6.
  5. Этот рекурсивный вызов создает новый фрейм поверх стека вызовов, представляющий процесс поиска в правом дочернем элементе корневого узла.
  6. Точно так же мы сравниваем значение 6 со значением текущего узла, которое равно 8. Поскольку 6 меньше 8, мы перемещаемся в левое поддерево.
  7. Мы рекурсивно вызываем функцию search с левым потомком правого потомка корневого узла (значение 6) и значением 6.
  8. Этот рекурсивный вызов создает еще один фрейм поверх стека вызовов, представляющий процесс поиска в левом дочернем элементе правого дочернего элемента корневого узла.
  9. Теперь мы сравниваем значение 6 со значением текущего узла, которое также равно 6. Мы нашли узел с искомым значением.
  10. Рекурсивный вызов для этого кадра возвращает узел со значением 6, что свидетельствует об успешном поиске, и кадр удаляется из стека вызовов.
  11. Рекурсивный вызов предыдущего кадра (правого потомка корневого узла) также возвращает найденный узел, и этот кадр также удаляется из стека вызовов.
  12. Наконец, первоначальный вызов функции search в корневом узле в качестве результата получает найденный узел, завершая процесс поиска.

Следуя этому процессу, мы успешно находим узел со значением 6 в BST.

Пример использования

Теперь давайте продемонстрируем, как использовать функции вставки и поиска в BST:

# Create an empty BST
root = None

# Insert nodes into the BST
root = insert(root, 50)
root = insert(root, 30)
root = insert(root, 70)
root = insert(root, 20)
root = insert(root, 40)
root = insert(root, 60)
root = insert(root, 80)

# Search for a value in the BST
result = search(root, 60)
if result:
    print("Value found in the BST")
else:
    print("Value not found in the BST")

Балансировка и оптимизация BST

Для обеспечения оптимальной производительности крайне важно поддерживать баланс BST. Несбалансированное дерево с большинством узлов на одной стороне снижает эффективность вставки, поиска и других операций. Различные алгоритмы, такие как деревья AVL и красно-черные деревья, поддерживают баланс, автоматически настраивая древовидную структуру во время вставки и удаления.

Кроме того, существуют методы дальнейшей оптимизации BST, такие как самобалансировка, обработка дубликатов и поточно-ориентированные модификации, в зависимости от конкретного варианта использования и требований.

Заключение

Двоичные деревья поиска обеспечивают эффективный способ хранения и извлечения данных в отсортированном виде. Используя свойство упорядочения дерева, операции вставки и поиска могут выполняться со средней временной сложностью O(log n). Однако важно контролировать и балансировать дерево, чтобы предотвратить снижение производительности. Понимание алгоритмов, лежащих в основе вставки и поиска BST, имеет основополагающее значение для любого программиста, работающего со структурами данных и алгоритмами, поскольку оно позволяет эффективно манипулировать отсортированными данными.

Эта статья является частью серии о бинарном дереве поиска, остальные статьи читайте здесь: