Patricia Trie (Radix Tree / PATRICIA)

Что такое Patricia Trie

PATRICIA — Practical Algorithm To Retrieve Information Coded In Alphanumeric.
Предложена Дональдом Морриссоном в 1968 году.

Patricia Trie = сжатый Trie, в котором цепочки узлов с одним ребёнком сжимаются в одно ребро с меткой (строкой, а не символом). Это устраняет главную проблему обычного Trie — избыточные узлы.


Ключевая идея: сжатие путей

Обычный Trie для "interview", "internal", "intro":

root → i → n → t → e → r → v → i → e → w ✓
                          ↘ n → a → l ✓
               ↘ r → o ✓

Patricia Trie:

root → "int" → "er" → "view" ✓
                    ↘ "nal"  ✓
            ↘ "ro"  ✓

Визуализация

Слова: cat, car, card, care, bat, bad

graph TD Root(("∅")) Root -->|"ca"| CA(("ca")) Root -->|"ba"| BA(("ba")) CA -->|"t"| CAT(("t ✓\n'cat'")) CA -->|"r"| CAR(("r ✓\n'car'")) BA -->|"t"| BAT(("t ✓\n'bat'")) BA -->|"d"| BAD(("d ✓\n'bad'")) CAR -->|"d"| CARD(("d ✓\n'card'")) CAR -->|"e"| CARE(("e ✓\n'care'"))

Каждое ребро теперь несёт строку, а не один символ


Структура узла

┌──────────────────────────────────────┐
│  children: map<string → Node>        │  ← ключ ребра — строка (метка)
│  is_end: bool                        │
│  value: any                          │
└──────────────────────────────────────┘

Основные операции

➕ Вставка — O(m)

При вставке возможны три ситуации:

Случай 1: Метка совпадает полностью

Есть ребро "car", вставляем "car" → просто is_end = true

Случай 2: Новое слово — префикс метки

Есть ребро "card", вставляем "car":
  Разбиваем "card" → "car" (is_end=true) + "d" (is_end=true, дочерний)

Случай 3: Нужно разбить метку

Есть ребро "card", вставляем "care":
  Общий префикс: "car"
  Разбиваем → "car" → "d" ✓
                     ↘ "e" ✓
def insert(node, word):
    for label, child in node.children.items():
        common = longest_common_prefix(label, word)
        if not common:
            continue
        if common == label:                  # случай 1/2
            insert(child, word[len(common):])
        else:                                # случай 3 — split
            split_node = Node()
            split_node.children[label[len(common):]] = child
            node.children[common] = split_node
            node.children.pop(label)
            if word == common:
                split_node.is_end = True
            else:
                split_node.children[word[len(common):]] = Node(is_end=True)
        return
    node.children[word] = Node(is_end=True)  # новая ветка

🔍 Поиск — O(m)

Поиск "care" в Patricia Trie:
  root: смотрим дочерние метки
  Совпадает "ca" → переходим, остаток "re"
  Совпадает "r"  → переходим, остаток "e"
  Совпадает "e"  → переходим, остаток ""
  is_end = true  → НАЙДЕНО ✓

➖ Удаление — O(m)

После удаления узла — объединяем родителя и единственного оставшегося ребёнка:

Удаляем "card", остаётся только "care":
  Узел "car" → {"d": ✓, "e": ✓}
  После удаления "d":
  Узел "car" → {"e": ✓}  →  схлопываем в "care" ✓

Trie vs Patricia Trie — сравнение

{
  "title": { "text": "Количество узлов: Trie vs Patricia", "left": "center" },
  "tooltip": { "trigger": "axis" },
  "legend": { "data": ["Trie", "Patricia Trie"], "bottom": 0 },
  "xAxis": {
    "type": "category",
    "name": "Число слов",
    "data": ["10", "50", "100", "500", "1000"]
  },
  "yAxis": {
    "type": "value",
    "name": "Узлов (примерно)"
  },
  "series": [
    {
      "name": "Trie",
      "type": "line",
      "data": [60, 310, 650, 3400, 7000],
      "smooth": true,
      "lineStyle": { "color": "#d9534f", "width": 3 }
    },
    {
      "name": "Patricia Trie",
      "type": "line",
      "data": [19, 99, 199, 999, 1999],
      "smooth": true,
      "lineStyle": { "color": "#5cb85c", "width": 3 }
    }
  ]
}
Trie Patricia Trie
Узлов на слово ~длина слова ~2 (max)
Ребро несёт 1 символ строку
Память ❌ много пустых узлов ✅ компактно
Скорость поиска O(m) O(m)
Разбиение узлов Нет Да (при вставке)
Реализация Проще Сложнее
Prefix scan

Варианты и применение

Radix Tree (Radix Trie)

Обобщение Patricia Trie — сжатие по группам битов (radix 2, 4, 8...).
Используется в ядре Linux для хранения адресного пространства процессов.

Где применяется Patricia / Radix Tree

Область Пример
🌐 IP-маршрутизация Longest Prefix Match в роутерах
🔤 Автодополнение IDE, поисковые строки
🗄️ Базы данных Индексы строковых полей
🐧 Linux Kernel radix_tree — page cache, IDR
🦀 Rust / Go std BTreeMap внутри использует похожие идеи
🔑 DNS Поиск по доменным именам

Итоговое сравнение всех трёх структур

B-Tree Trie Patricia Trie
Ключи любые (ord.) строки строки
Поиск O(log n) O(m) O(m)
Память умеренная большая компактная
Prefix search
Disk-friendly ✅ лучший
Балансировка автоматическая не нужна не нужна
Применение СУБД, ФС словари, NLP роутинг, ядро ОС

Input

добавляем описание Binary-Heap :)

Result

Binary Heap (Двоичная куча)

Что такое Binary Heap

Binary Heap — полное двоичное дерево, удовлетворяющее свойству кучи: каждый узел больше (или меньше) своих потомков. Это делает кучу идеальной структурой для реализации приоритетных очередей.

Предложена Дж. Уильямсом в 1964 году как основа алгоритма HeapSort.


Два вида кучи

graph LR A["Max-Heap\n(родитель ≥ детей)\nКорень = максимум"] B["Min-Heap\n(родитель ≤ детей)\nКорень = минимум"] A ~~~ B
Max-Heap Min-Heap
Свойство parent ≥ child parent ≤ child
Корень максимальный элемент минимальный элемент
Применение HeapSort, top-K Dijkstra, планировщики

Ключевые свойства

Свойство Значение
Тип дерева Полное двоичное (все уровни заполнены кроме последнего)
Последний уровень заполняется слева направо
Хранение обычно в массиве (без указателей)
Высота дерева O(log n)
Порядок между братьями не определён

Хранение в массиве

Главная особенность: дерево хранится в обычном массиве, без указателей:

        1
       / \
      3   2
     / \ / \
    7  4 5  6

Массив: [ _, 1, 3, 2, 7, 4, 5, 6 ]  (индекс с 1)
            0  1  2  3  4  5  6  7

Навигация по индексам:

Для узла с индексом i:
┌─────────────────────────────────┐
│  Родитель   →  i / 2            │
│  Левый сын  →  2 * i            │
│  Правый сын →  2 * i + 1        │
└─────────────────────────────────┘

(при индексации с 0: parent = (i-1)/2, left = 2i+1, right = 2i+2)

Визуализация Max-Heap

graph TD N1(("100")) N2(("85")) N3(("90")) N4(("70")) N5(("60")) N6(("75")) N7(("80")) N8(("40")) N9(("50")) N10(("55")) N1 --> N2 N1 --> N3 N2 --> N4 N2 --> N5 N3 --> N6 N3 --> N7 N4 --> N8 N4 --> N9 N5 --> N10
Массив: [100, 85, 90, 70, 60, 75, 80, 40, 50, 55]
Индекс:   0    1   2   3   4   5   6   7   8   9

Основные операции

🔼 Sift Up (всплытие) — вспомогательная

Используется после вставки: новый элемент добавляется в конец и «всплывает» вверх.

Вставка 95 в Max-Heap:

[100, 85, 90, 70, 60, 75, 80, 40, 50, 55, 95]
                                           ↑ добавили в конец

95 > 60 (родитель) → меняем местами
95 > 85 (родитель) → меняем местами
95 < 100 (родитель) → стоп

Результат: [100, 95, 90, 70, 85, 75, 80, 40, 50, 55, 60]
def sift_up(heap, i):
    while i > 0:
        parent = (i - 1) // 2
        if heap[i] > heap[parent]:      # для Max-Heap
            heap[i], heap[parent] = heap[parent], heap[i]
            i = parent
        else:
            break

🔽 Sift Down (погружение) — вспомогательная

Используется после удаления корня: последний элемент ставится на место корня и «тонет» вниз.

Удаление корня 100:
  Ставим последний элемент (60) на место корня
  [60, 95, 90, 70, 85, 75, 80, 40, 50, 55]

  60 < max(95, 90) = 95 → меняем с левым
  [95, 60, 90, 70, 85, 75, 80, 40, 50, 55]

  60 < max(70, 85) = 85 → меняем с правым
  [95, 85, 90, 70, 60, 75, 80, 40, 50, 55]

  60 > нет детей → стоп  ✓
def sift_down(heap, i, n):
    while True:
        largest = i
        left, right = 2*i + 1, 2*i + 2
        if left < n and heap[left] > heap[largest]:
            largest = left
        if right < n and heap[right] > heap[largest]:
            largest = right
        if largest == i:
            break
        heap[i], heap[largest] = heap[largest], heap[i]
        i = largest

➕ Вставка — O(log n)

1. Добавить элемент в конец массива
2. Sift Up от добавленного элемента
def push(heap, val):
    heap.append(val)
    sift_up(heap, len(heap) - 1)

🏆 Получить максимум/минимум — O(1)

def peek(heap):
    return heap[0]    # просто смотрим корень

🗑️ Удаление корня (Extract Max/Min) — O(log n)

1. Запомнить корень (результат)
2. Поставить последний элемент на место корня
3. Уменьшить размер кучи на 1
4. Sift Down от корня
def pop(heap):
    heap[0], heap[-1] = heap[-1], heap[0]
    val = heap.pop()
    sift_down(heap, 0, len(heap))
    return val

🏗️ Построение кучи из массива — O(n)

Наивный способ (n вставок) = O(n log n)
Оптимальный способ (Флойд, 1964) = O(n) — sift_down от середины к корню:

def heapify(arr):
    n = len(arr)
    # Начинаем с последнего внутреннего узла
    for i in range(n // 2 - 1, -1, -1):
        sift_down(arr, i, n)
Почему O(n)? Большинство узлов — листья (n/2 штук), 
они делают 0 операций. Узлы выше делают O(1), O(2)...
Суммарно: O(n) по формуле геометрической прогрессии.

HeapSort — O(n log n)

Куча лежит в основе классического алгоритма сортировки:

1. Построить Max-Heap из массива     → O(n)
2. n раз: извлечь максимум           → O(n log n)
   - поменять корень с последним
   - уменьшить размер кучи
   - sift_down

Сортировка на месте, без доп. памяти!
def heap_sort(arr):
    n = len(arr)
    heapify(arr)                           # O(n)
    for i in range(n - 1, 0, -1):
        arr[0], arr[i] = arr[i], arr[0]   # корень в конец
        sift_down(arr, 0, i)              # O(log n)

Сложность операций

{
  "title": { "text": "Сложность операций Binary Heap", "left": "center" },
  "tooltip": { "trigger": "axis" },
  "legend": { "data": ["O(1)", "O(log n)", "O(n)"], "bottom": 0 },
  "xAxis": {
    "type": "category",
    "data": ["1K", "10K", "100K", "1M", "10M"]
  },
  "yAxis": {
    "type": "value",
    "name": "Операций"
  },
  "series": [
    {
      "name": "O(1)",
      "type": "line",
      "data": [1, 1, 1, 1, 1],
      "smooth": true,
      "lineStyle": { "color": "#5cb85c", "width": 3 }
    },
    {
      "name": "O(log n)",
      "type": "line",
      "data": [10, 13, 17, 20, 23],
      "smooth": true,
      "lineStyle": { "color": "#f0ad4e", "width": 3 }
    },
    {
      "name": "O(n)",
      "type": "line",
      "data": [1000, 10000, 100000, 1000000, 10000000],
      "smooth": true,
      "lineStyle": { "color": "#d9534f", "width": 2, "type": "dashed" }
    }
  ]
}
Операция Сложность Примечание
Peek (min/max) O(1) просто heap[0]
Push O(log n) sift up
Pop (extract) O(log n) sift down
Heapify O(n) алгоритм Флойда
HeapSort O(n log n)
Поиск произвольного O(n) куча не поддерживает
Удаление произвольного O(log n) нужно знать индекс

Плюсы и минусы

✅ Плюсы ❌ Минусы
Min/Max за O(1) Поиск произвольного — O(n)
Компактное хранение в массиве Нет эффективного обхода в порядке
Нет накладных расходов на указатели Не подходит для range queries
Cache-friendly (массив) Нестабильная структура порядка
Heapify за O(n) Merge двух куч — O(n)

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

Область Пример
📊 Алгоритмы графов Dijkstra, Prim — Min-Heap для выбора следующей вершины
🗓️ Планировщики задач ОС выбирает следующую задачу по приоритету
📡 Потоковые данные Top-K элементов в реальном времени
🔀 K-way merge Слияние K отсортированных массивов
🗜️ Сжатие данных Алгоритм Хаффмана — строится через Min-Heap
🐍 Python heapq — встроенная Min-Heap
☕ Java PriorityQueue — Min-Heap по умолчанию

Итоговое сравнение всех четырёх структур

B-Tree Trie Patricia Trie Binary Heap
Тип ключей любые строки строки любые (ord.)
Поиск O(log n) O(m) O(m) O(n)
Min/Max O(log n) O(1)
Вставка O(log n) O(m) O(m) O(log n)
Prefix search
Память умеренная большая компактная минимальная
Disk-friendly
Применение СУБД, ФС словари, NLP роутинг, ядро ОС очереди, сортировка

Input

добавляем Бинарное дерево поиска (BST)

Result

Бинарное дерево поиска (BST)

Что такое BST

Binary Search Tree — двоичное дерево, в котором для каждого узла выполняется инвариант BST:

Все ключи в левом поддереве  <  ключ узла
Все ключи в правом поддереве >  ключ узла

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


Основные свойства

Свойство Значение
Тип дерева Двоичное (max 2 ребёнка)
Инвариант left < node < right
Высота (сбалансированное) O(log n)
Высота (вырожденное) O(n)
Хранение Узлы с указателями
Обход In-Order даёт отсортированную последовательность

Структура узла

┌──────────────────────────────────┐
│  key:   int/any                  │  ← ключ для сравнения
│  value: any        (опционально) │  ← полезная нагрузка
│  left:  *Node                    │  ← левый потомок (< key)
│  right: *Node                    │  ← правый потомок (> key)
│  parent: *Node     (опционально) │  ← родитель
└──────────────────────────────────┘

Визуализация

Вставка ключей в порядке: 50, 30, 70, 20, 40, 60, 80

graph TD N50(("50")) N30(("30")) N70(("70")) N20(("20")) N40(("40")) N60(("60")) N80(("80")) N50 -->|"left <"| N30 N50 -->|"right >"| N70 N30 -->|"left <"| N20 N30 -->|"right >"| N40 N70 -->|"left <"| N60 N70 -->|"right >"| N80

In-Order обход: 20 → 30 → 40 → 50 → 60 → 70 → 80


Основные операции

🔍 Поиск — O(log n) / O(n)

На каждом шаге отбрасываем половину дерева:

Поиск 40:
  50: 40 < 50 → идём влево
  30: 40 > 30 → идём вправо
  40: 40 == 40 → НАЙДЕНО ✓

Поиск 45:
  50 → 30 → 40 → нет правого ребёнка → НЕ НАЙДЕНО ✗
def search(node, key):
    if node is None or node.key == key:
        return node
    if key < node.key:
        return search(node.left, key)
    return search(node.right, key)

➕ Вставка — O(log n) / O(n)

Спускаемся как при поиске, вставляем в найденное пустое место:

Вставка 35:
  50: 35 < 50 → влево
  30: 35 > 30 → вправо
  40: 35 < 40 → влево
  None → вставляем сюда!
def insert(node, key):
    if node is None:
        return Node(key)
    if key < node.key:
        node.left = insert(node.left, key)
    elif key > node.key:
        node.right = insert(node.right, key)
    # key == node.key: дубликат, игнорируем
    return node
graph TD N50(("50")) N30(("30")) N70(("70")) N20(("20")) N40(("40")) N60(("60")) N80(("80")) N35(("35 🆕")) N50 --> N30 N50 --> N70 N30 --> N20 N30 --> N40 N40 --> N35 N40 --> NONE(" ") N70 --> N60 N70 --> N80 style N35 fill:#5cb85c,color:#fff style NONE fill:#fff,stroke:#fff

➖ Удаление — O(log n) / O(n)

Три случая в зависимости от числа детей удаляемого узла:

Случай 1 — узел является листом:

Удаляем 20 (нет детей):
  Просто убираем узел
  30.left = None

Случай 2 — узел имеет одного ребёнка:

Удаляем 60 (один ребёнок):
  Заменяем узел его единственным ребёнком
  70.left = child(60)

Случай 3 — узел имеет двух детей:

Удаляем 30 (два ребёнка 20 и 40):

  Находим In-Order Successor (наименьший в правом поддереве)
  или In-Order Predecessor (наибольший в левом поддереве)

  In-Order Successor для 30 → 35
  Копируем ключ 35 в узел 30
  Удаляем узел 35 (он уже без левого ребёнка → случай 1 или 2)
graph TD subgraph После["После удаления 30"] A50(("50")) A35(("35 ✓")) A70(("70")) A20(("20")) A40(("40")) A60(("60")) A80(("80")) A50 --> A35 A50 --> A70 A35 --> A20 A35 --> A40 A70 --> A60 A70 --> A80 style A35 fill:#f0ad4e,color:#fff end
def delete(node, key):
    if node is None:
        return None
    if key < node.key:
        node.left = delete(node.left, key)
    elif key > node.key:
        node.right = delete(node.right, key)
    else:
        # Случай 1 и 2
        if node.left is None:
            return node.right
        if node.right is None:
            return node.left
        # Случай 3: находим In-Order Successor
        successor = find_min(node.right)
        node.key = successor.key
        node.right = delete(node.right, successor.key)
    return node

def find_min(node):
    while node.left:
        node = node.left
    return node

Обходы дерева

graph TD T50(("50")) T30(("30")) T70(("70")) T20(("20")) T40(("40")) T60(("60")) T80(("80")) T50 --> T30 T50 --> T70 T30 --> T20 T30 --> T40 T70 --> T60 T70 --> T80
Обход Порядок Результат Применение
In-Order left → node → right 20 30 40 50 60 70 80 Сортировка
Pre-Order node → left → right 50 30 20 40 70 60 80 Копирование дерева
Post-Order left → right → node 20 40 30 60 80 70 50 Удаление дерева
Level-Order по уровням (BFS) 50 30 70 20 40 60 80 Сериализация
def inorder(node):
    if node:
        inorder(node.left)
        print(node.key)       # Left → Node → Right
        inorder(node.right)

def preorder(node):
    if node:
        print(node.key)       # Node → Left → Right
        preorder(node.left)
        preorder(node.right)

def postorder(node):
    if node:
        postorder(node.left)
        postorder(node.right)
        print(node.key)       # Left → Right → Node

Проблема вырождения

Главный недостаток BST — зависимость от порядка вставки:

graph LR subgraph Плохо["Вставка: 10, 20, 30, 40, 50 (вырожденное)"] D10(("10")) D20(("20")) D30(("30")) D40(("40")) D50(("50")) D10 -->|right| D20 D20 -->|right| D30 D30 -->|right| D40 D40 -->|right| D50 end
Вырожденное дерево = связный список
Поиск: O(n) вместо O(log n) 😱

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

Структура Гарантия Метод балансировки
AVL Tree h ≤ 1.44 log n Повороты, строгий баланс
Red-Black Tree h ≤ 2 log n Раскраска + повороты
Splay Tree амортизированный O(log n) Splay-операция
Treap ожидаемый O(log n) BST + Heap + случайность

Min / Max / Successor / Predecessor

# Минимум — самый левый узел
def find_min(node):
    while node.left:
        node = node.left
    return node

# Максимум — самый правый узел
def find_max(node):
    while node.right:
        node = node.right
    return node

# In-Order Successor (следующий по возрастанию)
def successor(node):
    if node.right:
        return find_min(node.right)
    # Идём вверх пока не выйдем из правого поддерева
    parent = node.parent
    while parent and node == parent.right:
        node, parent = parent, parent.parent
    return parent

Сложность операций

{
  "title": { "text": "BST: сбалансированное vs вырожденное", "left": "center" },
  "tooltip": { "trigger": "axis" },
  "legend": { "data": ["O(log n) — сбалансированное", "O(n) — вырожденное"], "bottom": 0 },
  "xAxis": {
    "type": "category",
    "name": "Число элементов",
    "data": ["100", "1K", "10K", "100K", "1M"]
  },
  "yAxis": {
    "type": "value",
    "name": "Операций"
  },
  "series": [
    {
      "name": "O(log n) — сбалансированное",
      "type": "line",
      "data": [7, 10, 13, 17, 20],
      "smooth": true,
      "lineStyle": { "color": "#5cb85c", "width": 3 }
    },
    {
      "name": "O(n) — вырожденное",
      "type": "line",
      "data": [100, 1000, 10000, 100000, 1000000],
      "smooth": true,
      "lineStyle": { "color": "#d9534f", "width": 3, "type": "dashed" }
    }
  ]
}
Операция В среднем Худший случай
Поиск O(log n) O(n)
Вставка O(log n) O(n)
Удаление O(log n) O(n)
Min / Max O(log n) O(n)
In-Order обход O(n) O(n)
Построение O(n log n) O(n²)

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

Область Пример
🗄️ Базы данных Основа для AVL и Red-Black деревьев в индексах
🐍 Python sortedcontainers.SortedList
☕ Java TreeMap, TreeSet — Red-Black Tree
🦀 Rust BTreeMap — B-Tree (обобщение BST)
➕ C++ STL std::map, std::set — Red-Black Tree
🔢 Алгоритмы Order statistics, range queries

Итоговое сравнение всех пяти структур

B-Tree Trie Patricia Binary Heap BST
Тип ключей любые строки строки любые любые
Поиск O(log n) O(m) O(m) O(n) O(log n)*
Min / Max O(log n) O(1) O(log n)*
Вставка O(log n) O(m) O(m) O(log n) O(log n)*
Удаление O(log n) O(m) O(m) O(log n) O(log n)*
Prefix search
Сортир. обход
Память умеренная большая компактная минимальная умеренная
Гарантии ✅ всегда ✅ всегда ✅ всегда ✅ всегда ⚠️ зависит от данных
Disk-friendly
Применение СУБД, ФС словари роутинг приоритеты основа AVL/RB

* — O(log n) только для сбалансированного дерева; O(n) в худшем случае

Input

Бинарное дерево поиска (BST) vs Binary Heap

Result

BST vs Binary Heap

Главный вопрос

Обе структуры — двоичные деревья с упорядоченными элементами. Но они решают разные задачи и имеют принципиально разные инварианты.


Инвариант — ключевое различие

graph TD subgraph BST["BST — инвариант по горизонтали"] B50(("50")) B30(("30")) B70(("70")) B20(("20")) B40(("40")) B60(("60")) B80(("80")) B50 --> B30 B50 --> B70 B30 --> B20 B30 --> B40 B70 --> B60 B70 --> B80 end subgraph HEAP["Max-Heap — инвариант по вертикали"] H80(("80")) H70(("70")) H60(("60")) H50(("50")) H40(("40")) H30(("30")) H20(("20")) H80 --> H70 H80 --> H60 H70 --> H50 H70 --> H40 H60 --> H30 H60 --> H20 end
BST Binary Heap
Инвариант left < node < right parent ≥ children (Max)
Направление порядка горизонтальное (лево/право) вертикальное (верх/низ)
Между братьями строгий порядок порядок не определён
Структура произвольная высота всегда полное дерево

Один и тот же массив — два разных дерева

Данные: [20, 30, 40, 50, 60, 70, 80]
graph LR subgraph bst2["BST (вставка по порядку — вырождение!)"] S20(("20")) S30(("30")) S40(("40")) S50(("50")) S60(("60")) S70(("70")) S80(("80")) S20 -->|right| S30 S30 -->|right| S40 S40 -->|right| S50 S50 -->|right| S60 S60 -->|right| S70 S70 -->|right| S80 end
graph TD subgraph heap2["Max-Heap (всегда сбалансирован)"] H80(("80")) H70(("70")) H60(("60")) H50(("50")) H40(("40")) H30(("30")) H20(("20")) H80 --> H70 H80 --> H60 H70 --> H50 H70 --> H40 H60 --> H30 H60 --> H20 end

BST вырождается в список — O(n) для всех операций.
Heap всегда остаётся полным деревом — гарантированный O(log n).


Детальное сравнение операций

🔍 Поиск произвольного элемента

BST — поиск 40:
  50 → 30 → 40 ✓   (3 шага = O(log n))

  Каждый шаг отсекает половину дерева —
  левое или правое поддерево

Heap — поиск 40:
  80 → проверяем 70 и 60
     → проверяем 50, 40... ✓  но могли пойти и вправо

  Нет информации о горизонтальном порядке —
  нужно обойти всё дерево в худшем случае

Вывод: BST выигрывает — O(log n) vs O(n)


🏆 Получить максимум / минимум

Max-Heap — получить максимум:
  return heap[0]   ← просто корень, O(1) ✓

BST — получить максимум:
  идём по правым детям до конца
  O(log n) для сбалансированного
  O(n) для вырожденного

Min-Heap — получить минимум:
  return heap[0]   ← O(1) ✓

BST — получить минимум:
  идём по левым детям до конца
  O(log n) / O(n)

Вывод: Heap выигрывает — O(1) vs O(log n)


➕ Вставка

BST — вставка 45:
  50 → 30 → 40 → правый ребёнок пуст → вставляем
  Но! Может понадобиться ребалансировка (AVL/RB)

Heap — вставка 45:
  Добавляем в конец массива → sift up
  Гарантированно O(log n), без ребалансировки
BST (обычный) BST (AVL/RB) Heap
Вставка O(log n)* O(log n) O(log n)
Гарантия
Сложность реализации низкая высокая низкая

➖ Удаление

Heap — удаление произвольного:
  1. Нужно НАЙТИ элемент → O(n)
  2. Только потом удалить → O(log n)
  Итого: O(n) — плохо!

  Удаление корня (min/max) — O(log n) ✓

BST — удаление любого:
  1. Найти → O(log n)
  2. Удалить (3 случая) → O(log n)
  Итого: O(log n) ✓

Вывод: BST выигрывает для произвольного удаления


📊 Хранение в памяти

Heap → массив (идеально):

  Индекс:  0   1   2   3   4   5   6
  Данные: [80, 70, 60, 50, 40, 30, 20]

  parent(i) = (i-1) / 2
  left(i)   = 2*i + 1
  right(i)  = 2*i + 2

  ✅ Нет указателей
  ✅ Cache-friendly
  ✅ Минимум памяти

BST → узлы с указателями:

  Node { key, left*, right*, parent* }

  ❌ Каждый узел = отдельная аллокация
  ❌ Указатели = доп. память
  ❌ Cache-miss при обходе

Визуальное сравнение сложностей

{
  "title": { "text": "Сравнение операций: BST vs Binary Heap", "left": "center" },
  "tooltip": { "trigger": "axis" },
  "legend": { "data": ["BST (сбалансированный)", "Binary Heap"], "bottom": 0 },
  "xAxis": {
    "type": "category",
    "data": ["Поиск любого", "Min/Max", "Вставка", "Удаление Min/Max", "Удаление любого", "Построение"]
  },
  "yAxis": {
    "type": "value",
    "name": "Условная сложность (n=1000)",
    "max": 1100
  },
  "series": [
    {
      "name": "BST (сбалансированный)",
      "type": "bar",
      "data": [10, 10, 10, 10, 10, 10000],
      "itemStyle": { "color": "#5b9bd5" },
      "label": {
        "show": true,
        "position": "top",
        "formatter": ["O(log n)", "O(log n)", "O(log n)", "O(log n)", "O(log n)", "O(n log n)"]
      }
    },
    {
      "name": "Binary Heap",
      "type": "bar",
      "data": [1000, 1, 10, 10, 1000, 1000],
      "itemStyle": { "color": "#f0ad4e" },
      "label": {
        "show": true,
        "position": "top",
        "formatter": ["O(n)", "O(1)", "O(log n)", "O(log n)", "O(n)", "O(n)"]
      }
    }
  ]
}

Итоговая таблица

Операция BST Binary Heap Победитель
Поиск произвольного O(log n) O(n) 🏆 BST
Получить Min/Max O(log n) O(1) 🏆 Heap
Вставка O(log n) O(log n) 🤝 Ничья
Удаление Min/Max O(log n) O(log n) 🤝 Ничья
Удаление произвольного O(log n) O(n) 🏆 BST
In-Order обход O(n) ✅ O(n log n) ❌ 🏆 BST
Построение O(n log n) O(n) 🏆 Heap
Память больше меньше 🏆 Heap
Cache-friendly 🏆 Heap
Гарантия O(log n) ⚠️ только AVL/RB ✅ всегда 🏆 Heap
Реализация сложнее проще 🏆 Heap

Когда что выбирать

flowchart TD Q1{"Нужен только\nMin или Max?"} Q1 -->|Да| Q2{"Нужно ещё\nудаление\nпроизвольного?"} Q1 -->|Нет| Q4{"Нужен\nпоиск / обход\nпо порядку?"} Q2 -->|Нет| HEAP["✅ Binary Heap\nПриоритетная очередь"] Q2 -->|Да| Q3{"Важна\nскорость?"} Q3 -->|"O(log n) для всего"| BST2["✅ BST (AVL/RB)"] Q3 -->|Простота кода| HEAP2["✅ Heap + доп. структура"] Q4 -->|Да| BST["✅ BST (AVL/RB)\nRange queries, сортировка"] Q4 -->|Нет| Q5{"Данные\nотсортированы\nпри вставке?"} Q5 -->|Да| HEAP3["✅ Heap\nнет риска вырождения"] Q5 -->|Нет| BST3["✅ BST"] style HEAP fill:#5cb85c,color:#fff style HEAP2 fill:#5cb85c,color:#fff style HEAP3 fill:#5cb85c,color:#fff style BST fill:#5b9bd5,color:#fff style BST2 fill:#5b9bd5,color:#fff style BST3 fill:#5b9bd5,color:#fff

Типичные сценарии

Задача Выбор Причина
Приоритетная очередь Heap O(1) min/max, простота
Алгоритм Дейкстры Heap постоянно нужен Min
Планировщик задач ОС Heap приоритеты, O(1) извлечение
HeapSort Heap O(n log n), на месте
Top-K элементов Heap держим кучу размером K
Словарь / Map BST поиск по ключу O(log n)
Range query (от A до B) BST In-Order обход отрезка
Sorted Set / Ordered Map BST нужен полный порядок
Медиана потока данных Два Heap Min-Heap + Max-Heap

Реализации в стандартных библиотеках

Язык Heap BST
🐍 Python heapq (Min-Heap) sortedcontainers.SortedList
☕ Java PriorityQueue TreeMap / TreeSet (Red-Black)
➕ C++ priority_queue std::map / std::set (Red-Black)
🦀 Rust BinaryHeap BTreeMap / BTreeSet
🐹 Go container/heap — (нет в stdlib)
🟨 JS — (нет в stdlib) — (нет в stdlib)

Краткий вывод

┌─────────────────────────────────────────────────────┐
│                                                     │
│  Heap  = специалист по Min/Max                      │
│          «Дай мне наибольший/наименьший — быстро»   │
│          O(1) для главной операции                  │
│                                                     │
│  BST   = универсальный упорядоченный словарь        │
│          «Найди, вставь, удали что угодно»          │
│          O(log n) для всего, полный порядок         │
│                                                     │
└─────────────────────────────────────────────────────┘