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

В этом руководстве вы узнаете, как работает двоичное дерево поиска. Здесь же собраны примеры реализации дерева двоичного поиска на Си, C++, Java и Python.

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

  • Дерево двоичное, потому что у каждого узла не более двух дочерних элементов. 
  • Дерево поиска, потому что его можно использовать для проверки вхождения числа — за время O(log(n))

Чем отличается от обычного двоичного дерева

  1. Все узлы левого поддерева меньше корневого узла.
  2. Все узлы правого поддерева больше корневого узла.
  3. Оба поддерева каждого узла тоже являются деревьями двоичного поиска, т. е. также обладают первыми двумя свойствами. 

У правого дерева есть поддерево со значением 2, которое меньше, чем корень 3 — таким дерево двоичного поиска быть не может.

Операции с двоичным деревом поиска

Поиск элемента

Сложность в среднем: O(log n)
Сложность в худшем случае: O(n)

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

Если значение ниже корня, мы можем точно сказать, что оно находится не в правильном поддереве. Тогда нам нужно искать только в левом поддереве. А если значение выше корня, можно точно сказать, что значения нет в левом поддереве. Тогда ищем только в правом поддереве.

Алгоритм:
If root == NULL 
   return NULL;
If number == root->data
   return root->data;
If number < root->data
   return search(root->left)
If number > root->data
    return search(root->right)

Изобразим это на диаграмме.

• Не нашли 4 → идем по левому поддереву корня 8

• Не нашли 4 → идем по левому поддереву корня 3

 

• Не нашли 4 → идем по левому поддереву корня 6

• Нашли 4 — ура.

Если мы нашли значение, мы возвращаем его так, чтобы оно распространялось на каждом шаге рекурсии — рассмотрите рисунок ниже.

Как вы могли заметить, мы четыре раза вызывали search(struct node*). Когда мы возвращаем новый узел или NULL, значение возвращается снова и снова, пока search(root) не вернет окончательный результат.

Если мы не нашли значение, значит, мы достигли левого или правого дочернего элемента листового узла, который имеет значение NULL — это значение также рекурсивно распространяется и возвращается.

Вставка элемента

Сложность в среднем: O(log n)
Сложность в худшем случае: ΘO(n)

Операция вставки значения похожа на поиск элемента. Мы также придерживаемся правила: левое поддерево меньше корня, правое поддерево — больше. 

Мы продолжаем переходить либо к правому поддереву, либо к левому поддереву в зависимости от значения узла, и когда достигаем точки, в которой левое или правое поддерево имеет значение NULL, помещаем туда новый узел.

Алгоритм:
If node == NULL 
   return createNode(data)
if (data < node->data)
   node->left  = insert(node->left, data);
else if (data > node->data)
   node->right = insert(node->right, data);  
return node;

Алгоритм не такой простой, как может показаться на первый взгляд. Давайте визуализируем процесс. 

• 4 < 8 → идем через левого «ребенка» 8.

• 4 > 3 → идем через правого «ребенка» 3.

• 4 < 6 → идем через левого «ребенка» 6.

• Левого «ребенка» у 6 нет → вставляем 4 как левый дочерний элемент 6.

Мы вставили узел в нужное место, но нам всё еще нужно выйти из функции, не повредив при этом остальную часть дерева. Здесь пригодится return node;. В случае с NULL, вновь созданный узел возвращается и присоединяется к родительскому узлу, иначе тот же узел возвращается без каких-либо изменений по мере продвижения вверх, пока мы не вернемся к корню.

Таким образом, когда мы будем двигаться обратно вверх по дереву, связи других узлов не изменятся.

Корень возвращаем в конце — так ничего не сломается.

Удаление элемента

Сложность в среднем: O(log n)
Сложность в худшем случае: O(n)

Всего может быть три случая при удаление элемента из дерева двоичного поиска. Давайте рассмотрим каждый.

Случай I: лист

Первый случай — когда нужно удалить листовой элемент. Просто удаляем узел из дерева. 

• Нужно удалить 4.

• Просто удалили узел.

Случай II: есть дочерний элемент

Во втором случае у узла, который мы хотим удалить, есть один дочерний узел. В этом случае действуем так:

  1. Заменяем удаляемый узел на дочерний узел.
  2. Удаляем дочерний узел.

• Нужно удалить 6.

• Меняем 6 на 7 и удаляем узел 7.

•Нужный узел удален.

Случай III: два дочерних элемента

Если у узла, который мы хотим удалить, есть два потомка, делаем так:

  1. Получаем значение inorder-преемника этого узла.
  2. Заменяем удаляем узел на inorder-преемника.
  3. Удаляем inorder-преемника. 

• Хотим удалить 3.

• Копируем значение inorder-преемника — 4.

• Удаляем inorder-преемника.

Реализация на языках программирования

Python

# Операции с двоичным деревом поиска на Python

# Создаем узел
class Node:
    def __init__(self, key):
        self.key = key
        self.left = None
        self.right = None

# Отсортированный (inorder) обход
def inorder(root):
    if root is not None:

        # Обходим левое поддерево
        inorder(root.left)

        # Обходим корень
        print(str(root.key) + "->", end=' ')

        # Обходим правое поддерево
        inorder(root.right)

# Вставка элемента
def insert(node, key):
    # Возвращаем новый узел, если дерево пустое
    if node is None:
        return Node(key)

    # Идем в нужное место и вставляет узел
    if key < node.key:
        node.left = insert(node.left, key)
    else:
        node.right = insert(node.right, key)
    return node

# Поиск inorder-преемника
def minValueNode(node):
    current = node
    # Найдем крайний левый лист — он и будет inorder-преемником
    while(current.left is not None):
        current = current.left
    return current

# Удаление узла
def deleteNode(root, key):
    # Возвращаем, если дерево пустое
    if root is None:
        return root

    # Найдем узел, который нужно удалить
    if key < root.key:
        root.left = deleteNode(root.left, key)
    elif(key > root.key):
        root.right = deleteNode(root.right, key)
    else:
        # Если у узла только один дочерний узел или вообще их нет        
        if root.left is None:
            temp = root.right
            root = None
            return temp

        elif root.right is None:
            temp = root.left
            root = None
            return temp

        # Если у узла два дочерних узла,
        # помещаем центрированного преемника
        # на место узла, который нужно удалить
        temp = minValueNode(root.right)

        root.key = temp.key

        # Удаляем inorder-преемниа
        root.right = deleteNode(root.right, temp.key)

    return root

# Тестим функции
root = None
root = insert(root, 8)
root = insert(root, 3)
root = insert(root, 1)
root = insert(root, 6)
root = insert(root, 7)
root = insert(root, 10)
root = insert(root, 14)
root = insert(root, 4)

print("Отсортированный обход: ", end=' ')
inorder(root)

print("\nПосле удаления 10")
root = deleteNode(root, 10)

print("Отсортированный обход: ", end=' ')
inorder(root)

Java

// Операции с двоичным деревом поиска на Java

// Создаем узел
class BinarySearchTree {
  class Node {
    int key;
    Node left, right;
    public Node(int item) {
      key = item;
      left = right = null;
    }
  }

  Node root;

  BinarySearchTree() {
    root = null;
  }


  void insert(int key) {
    root = insertKey(root, key);
  }

  // Вставляем в дерево элемент
  Node insertKey(Node root, int key) {
    // Возвращаем новый узел, если дерево пустое 
    if (root == null) {
      root = new Node(key);
      return root;
    }

    // Идем в нужное место и вставляем узел
    if (key < root.key)
      root.left = insertKey(root.left, key);
    else if (key > root.key)
      root.right = insertKey(root.right, key);

    return root;
  }

  void inorder() {
    inorderRec(root);
  }

  // Отсортированный (inorder) обход 
  void inorderRec(Node root) {
    if (root != null) {
      inorderRec(root.left);
      System.out.print(root.key + " -> ");
      inorderRec(root.right);
    }
  }

  void deleteKey(int key) {
    root = deleteRec(root, key);
  }

  Node deleteRec(Node root, int key) {
    // Возвращаем, если дерево пустое
    if (root == null)
      return root;

    // Ищем узел, который нужно удалить
    if (key < root.key)
      root.left = deleteRec(root.left, key);
    else if (key > root.key)
      root.right = deleteRec(root.right, key);
    else {
      // Если у узла только один дочерний элемент или их вообще нет
      if (root.left == null)
        return root.right;
      else if (root.right == null)
        return root.left;

      // Если у узла два дочерних элемента
      // Помещаем inorder-преемника на место узла, который хотим удалить
      root.key = minValue(root.right);
      // Удаляем inorder-преемника
      root.right = deleteRec(root.right, root.key);
    }
    return root;
  }

  // Ищем inorder-преемника
  int minValue(Node root) {
    int minv = root.key;
    while (root.left != null) {
      minv = root.left.key;
      root = root.left;
    }
    return minv;
  }

  // Тестим функции
  public static void main(String[] args) {
    BinarySearchTree tree = new BinarySearchTree();
    tree.insert(8);
    tree.insert(3);
    tree.insert(1);
    tree.insert(6);
    tree.insert(7);
    tree.insert(10);
    tree.insert(14);
    tree.insert(4);

    System.out.print("Отсортированный обход: ");
    tree.inorder();

    System.out.println("\n\nПосле удаления 10");
    tree.deleteKey(10);

    System.out.print("Отсортированный обход: ");
    tree.inorder();
  }
}

C

// Операции с двоичным деревом поиска на C



#include <stdio.h>
#include <stdlib.h>
struct node {
  int key;
  struct node *left, *right;
};

// Создаем узел
struct node *newNode(int item) {
  struct node *temp = (struct node *)malloc(sizeof(struct node));
  temp->key = item;
  temp->left = temp->right = NULL;
  return temp;
}

// Отсортированный (inorder) обход 
void inorder(struct node *root) {
  if (root != NULL) {
    // Обходимо лево
    inorder(root->left);

    // Обходим корень
    printf("%d -> ", root->key);

    // Обходимо право
    inorder(root->right);
  }
}

// Вставка узла
struct node *insert(struct node *node, int key) {
  // Возвращаем новый узел, если дерево пустое
  if (node == NULL) return newNode(key);

  // Проходим в нужное место и вставляем узел
  if (key < node->key)
    node->left = insert(node->left, key);
  else
    node->right = insert(node->right, key);

  return node;
}

// Поиск inorder-преемника
struct node *minValueNode(struct node *node) {
  struct node *current = node;

  // Находим крайний левый лист — он и будет inorder-преемником
  while (current && current->left != NULL)
    current = current->left;

  return current;
}

// Удаляем узел
struct node *deleteNode(struct node *root, int key) {
  // Возвращаем, если дерево пустое
  if (root == NULL) return root;

  // Ищем узел, который нужно удалить
  if (key < root->key)
    root->left = deleteNode(root->left, key);
  else if (key > root->key)
    root->right = deleteNode(root->right, key);
  else {
    // Если у узла только один дочерний элемент или их вообще нет
    if (root->left == NULL) {
      struct node *temp = root->right;
      free(root);
      return temp;
    } else if (root->right == NULL) {
      struct node *temp = root->left;
      free(root);
      return temp;
    }

    // Если у узла два дочерних элемента
    struct node *temp = minValueNode(root->right);

    // Помещаем inorder-преемника на место узла, который хотим удалить
    root->key = temp->key;

    // Удаляем inorder-преемника
    root->right = deleteNode(root->right, temp->key);
  }
  return root;
}

// Тестим функции
int main() {
  struct node *root = NULL;
  root = insert(root, 8);
  root = insert(root, 3);
  root = insert(root, 1);
  root = insert(root, 6);
  root = insert(root, 7);
  root = insert(root, 10);
  root = insert(root, 14);
  root = insert(root, 4);

  printf("Отсортированный обход: ");
  inorder(root);

  printf("\nПосле удаления 10\n");
  root = deleteNode(root, 10);

  printf("Отсортированный обход: ");
  inorder(root);
}

C++

// Операции с двоичным деревом поиска на C++

#include <iostream>
using namespace std;

struct node {
  int key;
  struct node *left, *right;
};

// Создаем узел
struct node *newNode(int item) {
  struct node *temp = (struct node *)malloc(sizeof(struct node));
  temp->key = item;
  temp->left = temp->right = NULL;
  return temp;
}

// Отсортированный (inorder) обход
void inorder(struct node *root) {
  if (root != NULL) {
    // Обходим лево
    inorder(root->left);

    // Обходим корень
    cout << root->key << " -> ";

    // Обходим право
    inorder(root->right);
  }
}

// Вставка узла
struct node *insert(struct node *node, int key) {
  // Возвращаем новый узел, если дерево пустое
  if (node == NULL) return newNode(key);

  // Проходим в нужное место и вставляет узел
  if (key < node->key)
    node->left = insert(node->left, key);
  else
    node->right = insert(node->right, key);

  return node;
}

// Поис inorder-преемника
struct node *minValueNode(struct node *node) {
  struct node *current = node;

  // Ищем крайний левый лист — он и будет inorder-преемником
  while (current && current->left != NULL)
    current = current->left;

  return current;
}

// Удаление узла
struct node *deleteNode(struct node *root, int key) {
  // Возвращаем, если дерево пустое
  if (root == NULL) return root;

  // Ищем узел, который хотим удалить
  if (key < root->key)
    root->left = deleteNode(root->left, key);
  else if (key > root->key)
    root->right = deleteNode(root->right, key);
  else {
    // Если у узла один дочерний элемент или их нет
    if (root->left == NULL) {
      struct node *temp = root->right;
      free(root);
      return temp;
    } else if (root->right == NULL) {
      struct node *temp = root->left;
      free(root);
      return temp;
    }

    // Если у узла два дочерних элемента
    struct node *temp = minValueNode(root->right);

    // Помещаем inorder-преемника на место узла, который хотим удалить
    root->key = temp->key;

    // Удаляем inorder-преемника
    root->right = deleteNode(root->right, temp->key);
  }
  return root;
}

// Тестим функции
int main() {
  struct node *root = NULL;
  root = insert(root, 8);
  root = insert(root, 3);
  root = insert(root, 1);
  root = insert(root, 6);
  root = insert(root, 7);
  root = insert(root, 10);
  root = insert(root, 14);
  root = insert(root, 4);

  cout << "Отсортированный обход: ";
  inorder(root);

  cout << "\nПосле удаления 10 10\n";
  root = deleteNode(root, 10);

  cout << "Отсортированный обход: ";
  inorder(root);
}

Где используется

  1. При многоуровневой индексации в базе данных.
  2. Для динамической сортировки.
  3. Для управления областями виртуальной памяти в ядре Unix.
codechick

СodeСhick.io - простой и эффективный способ изучения программирования.

2024 ©