Заметки докладчика
Семинар 4.10

AVL · RB-tree · Min-heap

Подробный разбор AVL, RB и кучи для 1 курса

Алгоритмизация и программирование · 1 курс · 80 мин · после 4.09

4.10 · План курса

Дорожная карта

3.09Теория БДП
4.09Код БДП, метрики
4.10AVL, RB, куча
4.10 · Введение

Зачем этот семинар

Проблема с 4.09

БДП при вставке 1,2,3,4,5 превращается в цепочку. Поиск O(n) — нет выигрыша перед массивом.

Решение 4.10

  • AVL — жесткий баланс, повороты
  • RB-tree — мягче, перекраски + повороты
  • Min-heap — другая задача: быстрый минимум

Цель: h = O(log n) → поиск и вставка ≈ log₂ n шагов даже на «плохих» данных.

4.10 · Теория

O(log n) на пальцах

n (ключей)h в цепочке BSTh ≈ log₂ nВыигрыш
88≈ 3в 2–3 раза
1 0001 000≈ 10~100×
1 000 0001 000 000≈ 20~50 000×

log₂ n — сколько раз можно делить n пополам, пока не дойдем до 1. Сбалансированное дерево «режет» задачу примерно пополам на каждом шаге.

4.10 · Словарь

Словарь терминов — балансировка

ТерминЧто это такоеЗачем на семинаре
Вырождение БДППри вставке ключей по возрастанию дерево становится цепочкой (каждый узел — один ребенок). Высота h = n, поиск O(n).Мотивация для AVL и RB
БалансировкаПоддержание малой высоты дерева после вставок: локально «подкручиваем» форму, не меняя порядок inorder.Главная идея 4.10
AVL-деревоAdel'son-Vel'skii & Landis (1962). БДП + у каждого узла |bf| ≤ 1. Высота гарантированно O(log n). Чиним поворотами после вставки.См. слайд «AVL-дерево — что это»
Balance factor (bf)bf(v) = h(левого) − h(правого). + — левое поддерево выше, — правое. Норма: −1…+1; |bf|=2 → поворот.См. слайд «Balance factor — расшифровка»
Поворот (rotation)Перестановка 2–3 связанных узлов: меняется форма, но inorder не меняется (ключи по-прежнему отсортированы).Считаем в Rotations
LL / RR / LR / RLЧетыре типа дисбаланса. Буквы = куда «тяжелее»: у корня и у его сына (L/R). Прямые LL/RR — 1 поворот; ломаные LR/RL — 2 поворота.См. слайды «Четыре случая» и rebalance()
4.10 · Словарь

Словарь терминов — RB и куча

ТерминЧто это такоеЗачем на семинаре
RB-treeRed-Black tree — самобалансирующееся БДП: у узла цвет + 5 правил на цвета. Высота O(log n), обновления обычно дешевле AVL. Основа std::map в GCC.См. слайды «RB-tree — что это»
Перекраска (recoloring)Смена цвета узла с красного на черный или наоборот — способ восстановить правила RB без поворота.Метрика Recolorings
fixInsertПроцедура после вставки в RB: новый узел красный; если нарушены правила — перекраска, поворот или подъем к родителю.Ядро rbTree.cpp
Черная высотаЧисло черных узлов на пути от потомка узла до NIL-листа, не включая сам текущий узел. Для всех путей — одинаково.Одно из правил RB-дерева
Куча (heap)Complete-дерево в массиве: все уровни заполнены, последний — слева направо. Инвариант «кучи», а не БДП.См. слайд «Куча — что это»
Min-heapРодитель ≤ оба ребенка. Корень (индекс 0) — минимум. extract-min за O(log n), произвольный поиск — O(n).4.10.03 — minHeap.cpp
sift up / sift downПросеивание вверх — после вставки поднимаем элемент к корню. Просеивание вниз — после извлечения минимума опускаем новый корень.minHeap.cpp
4.10 · Повтор

Что берем с 4.09

✓ Используем

  • БДП, inorder, YES/NO
  • Высота, сравнения при поиске
  • Вырождение: 1,2,…,n → цепочка
  • GTest, формат заданий

✗ Не повторяем

  • Определение БДП
  • Три рекурсивных обхода
  • Построение по массиву
4.10 · Теория

Зачем балансировка

Проблема

BST при вставке 1,2,3,4,5:

  • высота h = n
  • поиск O(n) — как список

Цель

h = O(log n) при любых вставках

Средство: повороты (+ перекраски в RB)

Inorder не меняется!

1 2 3 4 5

h = 5

4.10 · AVL

AVL-дерево — что это

БДП + у каждого узла |bf| ≤ 1. Высота O(log n). Задание 4.10.01.

4.10 · Теория

Поворот вправо

До

y x z t

После

x z y t

Поворот влево — зеркально. Inorder сохраняется.

4.10 · AVL

Поворот вправо — алгоритм

ШагДействие
1x = y.left, t = x.right
2x.right = y
3y.left = t
4Обновить высоты; вернуть x

Inorder: z → x → t → y.

x = y.left; t = x.right;
x.right = y; y.left = t;
updateHeight(y); updateHeight(x);
return x;
4.10 · AVL

Поворот влево — алгоритм

ШагДействие
1y = x.right, t = y.left
2y.left = x
3x.right = t
4Обновить высоты; вернуть y

Inorder: x → t → y → z. Случай RR.

4.10 · AVL

Balance factor — расшифровка

bf(v) = height(left) − height(right)

bfСмыслДействие
−1…+1норма
+2левое на 2 вышеbalanceFactor(node->left) → LL/LR
−2правое на 2 вышеbalanceFactor(node->right) → RR/RL
4.10 · AVL

Четыре случая: LL, RR, LR, RL

СлучайУсловиеПовороты
LLbf=+2, bf(left)≥0rotateRight
RRbf=−2, bf(right)≤0rotateLeft
LRbf=+2, bf(left)<0rotateLeft(left), rotateRight
RLbf=−2, bf(right)>0rotateRight(right), rotateLeft
4.10 · AVL

LL и RR — прямые случаи

LL — 3, 2, 1

bf(3)=+2, bf(2)≥0 → rotateRight(3). Rotations: 1

RR — 1, 2, 3

bf(1)=−2, bf(2)≤0 → rotateLeft(1). Rotations: 1

4.10 · AVL

LR и RL — ломаные случаи

LR — 3, 1, 2

rotateLeft(1), затем rotateRight(3). Rotations: 2

RL — 1, 3, 2

rotateRight(3), затем rotateLeft(1). Rotations: 2

4.10 · AVL

Алгоритм rebalance

if bf > +1:
  if balanceFactor(node.left) < 0: node.left = rotateLeft(node.left)
  return rotateRight(node)
if bf < -1:
  if balanceFactor(node.right) > 0: node.right = rotateRight(node.right)
  return rotateLeft(node)
4.10 · AVL

Когда пересчитывать bf

Формула bf работает только если высоты актуальны. Порядок после вставки:

  1. Вставить ключ как в BST (спуск вниз)
  2. Подниматься к корню: на каждом узле updateHeight
  3. На каждом узле посчитать bf
  4. При |bf| = 2 — поворот по таблице случаев
  5. После поворота — снова updateHeight у затронутых узлов

Лист без детей: height = 1, bf = 0. Пустой сын в формуле дает вклад 0.

Частые ошибки с bf

  • Путают знак: +2 — тяжело слева, не справа
  • Считают bf до обновления высот после вставки
  • Не пересчитывают высоту после поворота → неверный следующий bf
  • При bf=+2 смотрят правого сына вместо левого

Забыли updateHeight — тесты падают с «не тем» числом поворотов.

4.10 · AVL

AVL — вставка целиком

  1. BST-спуск
  2. updateHeight + rebalance вверх
4.10 · RB-tree

RB-tree — что это

Красно-черное дерево (Guibas, Sedgewick) — самобалансирующееся БДП. Баланс мягче, чем у AVL, зато вставки и удаления обычно дешевле.

СвойствоСодержание
Как БДППорядок ключей: left < root < right, inorder отсортирован
ДополнительноУ каждого узла цвет: красный или черный
Высота2·log₂(n+1) — чуть выше AVL, но все равно O(log n)
ВосстановлениеПерекраска и/или поворот после вставки (fixInsert)
Где встречаетсяstd::map, std::set (GCC), Java TreeMap

Чем RB отличается от AVL

  • AVL следит за bf у каждого узла; RB — за цветами на путях
  • RB чаще обходится перекраской без поворота
  • Дерево RB может быть чуть выше, но дешевле обновлять
  • В коде нужны parent-ссылки для поворотов

Задания 4.10.02 и 4.10.04rbTree.cpp.

4.10 · RB-tree

RB-tree — пять правил

  1. Красный/черный
  2. Корень черный
  3. NIL черные
  4. Нет двух красных подряд
  5. Одинаковая черная высота
4.10 · RB-tree · раскраска

Почему новый узел красный

Новый узел ставится как в BST и красится в красный, потому что красный узел не увеличивает черную высоту пути.

Цвет нового узлаЧто ломается
черныйОдин путь получает лишний черный узел
красныйМожет появиться только нарушение «красный → красный»

fixInsert

  1. корень → черный
  2. черный родитель → стоп
  3. красный родитель → смотрим дядю
4.10 · RB-tree · раскраска

Как считать перекраски

СитуацияЦветаМетрика
первый узелroot: red → blackне считаем отдельно
родитель черныйничего+0
дядя красныйparent, uncle → black; grand → red+3
дядя черный, прямая цепочкаparent → black; grand → red+1 в reference
4.10 · RB-tree · пример

RB-дерево: корректный пример

10 5 15 3 7 12 18
  • Inorder остается отсортированным
  • Корень черный
  • У красных узлов нет красных детей
4.10 · RB-tree · пример

RB-вставка: дядя красный → перекраска

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

Recolorings += 3, Rotations += 0.

4.10 · RB-tree · пример

RB-вставка: дядя черный → поворот

Пример 1, 2, 3: перед исправлением цепочка 1(B) → 2(R) → 3(R). После rotateLeft(1): 2(B) с красными детьми 1 и 3.

Rotations += 1, Recolorings += 1 в этой реализации.

4.10 · RB-tree

RB: fixInsert после вставки

  1. Новый узел вставляем как в BST и красим в красный
  2. Если родитель черный — правила RB не нарушены
  3. Если родитель красный — появилось нарушение «красный → красный»
  4. fixInsert поднимается вверх, пока нарушение не исчезнет

fixInsert — три случая

СитуацияДействие
Родитель черныйНарушений нет — стоп
Родитель красный, дядя красныйПерекрасить родителя, дядю → черные; дедушку → красный; подняться к дедушке
Родитель красный, дядя черный (или нет)«Ломаная» цепочка → поворот у родителя; «прямая» → поворот у дедушки + перекраска

Дядя — брат родителя (левый/правый сын дедушки). Считаем перекраски в Recolorings.

4.10 · Сравнение

AVL vs Red-Black

AVLRed-Black
Жесткость балансаВыше (|bf|≤1)Мягче
ВысотаОбычно меньшеЧуть больше, но O(log n)
Вставка/удалениеЧасто больше поворотовОбычно дешевле
Типичное применениеЧастый поискЧастые вставки/удаления
Главный инструментbf + поворотыцвета + fixInsert
4.10 · Теория

BST · AVL · RB · куча — сводка

Таблица инвариантов, хранения, сложности поиска и минимума.

4.10 · Куча

Куча (heap) — что это

Куча — complete-бинарное дерево, хранящееся в массиве: все уровни заполнены, а последний заполняется слева направо. Главное правило — не порядок «слева меньше», а отношение родитель ↔ дети.

ТипИнвариантКорень
Min-heapродитель ≤ оба ребенкаминимум всего набора
Max-heapродитель ≥ оба ребенкамаксимум

На семинаре — только min-heap (minHeap.cpp).

Куча ≠ БДП ≠ AVL/RB

  • Нет правила «левое меньше правого» — только родитель vs дети
  • Inorder не дает сортировку ключей
  • Ищем произвольный ключ — O(n), не O(log n)
  • Зато минимум всегда в data[0] за O(1)

Применение: очередь с приоритетом, алгоритм Дейкстры, пирамидальная сортировка.

4.10 · Новое

Min-heap — структура и операции

Куча ≠ БДП! Другой инвариант: родитель ≤ дети. Произвольный поиск — O(n).
1 3 2 6 5 4
Массив: [1, 3, 2, 6, 5, 4] parent(i) = (i-1)/2 left(i) = 2i+1 right(i) = 2i+2 heapBuild → sift down снизу вверх extract-min → sift down от корня 2× extract → 1, 2
ОперацияАлгоритмВремя
heapBuildСкопировать ключи, siftDown с n/2−1 до 0O(n)
heapExtractMinВзять data[0], последний в корень, siftDown(0)O(log n)
Просмотр минимумаdata[0] без извлеченияO(1)

Задание 4.10.03: реализуем heapBuild и heapExtractMin. Heap built: YES печатается task-файлом после успешного heapBuild.

4.10 · Куча

sift up и sift down

sift up (идея для push)

Новый элемент в конец массива. Пока он меньше родителя — меняем местами с родителем и идем вверх.

Индекс родителя: (i-1)/2

sift down (при extract-min)

Корень (минимум) забираем. Последний элемент ставим в корень. Пока он больше меньшего ребенка — меняем именно с меньшим ребенком.

Дети: 2i+1, 2i+2

Массив [1,3,2,6,5,4] → extract → 1, затем extract → 2 (минимумы не убывают)
4.10 · Эксперимент

BST vs RB: одни ключи

Вставка 1 2 3 4 5, затем q поисков — задание 4.10.04

BST — цепочка

1 2 3 4 5

Height BST: 5

RB-tree — сбалансировано

2 1 4 3 5

Height RB: ≤ 3

Формат полного ввода-вывода для 4.10.04 теперь показан на слайде «Алгоритм: rbTree.cpp» под алгоритмом.

4.10 · Код · 4.10.01

Алгоритм: avlTree.cpp

lib/treeAlgs/avlTree.cpp — дописать тела функций

rotateRight(y, rot):
  ++(*rot); x = y.left; t = x.right
  x.right = y; y.left = t
  updateHeight(y); updateHeight(x)
  return x

rotateLeft(x, rot):  // зеркально
  ++(*rot); y = x.right; t = y.left
  y.left = x; x.right = t
  updateHeight(x); updateHeight(y)
  return y

rebalance(node, rot):
  updateHeight(node); bf = balanceFactor(node)
  if bf > +1:
    if balanceFactor(node.left) < 0:
      node.left = rotateLeft(node.left, rot)
    return rotateRight(node, rot)
  if bf < -1:
    if balanceFactor(node.right) > 0:
      node.right = rotateRight(node.right, rot)
    return rotateLeft(node, rot)
  return node
avlInsert(root, key, rot):
  if !root: return new AvlNode(key)
  if key < root.key:
    root.left = avlInsert(left, key, rot)
  else if key > root.key:
    root.right = avlInsert(right, key, rot)
  else: return root    // дубликат
  return rebalance(root, rot)

avlSize(root):
  if !root: return 0
  return 1 + avlSize(left) + avlSize(right)

avlInorder: как binInorder — left, key, right
avlFree: postorder delete (как binFree)

В draft пустые не только повороты: дописать также avlSize, avlInorder, avlFree. task41001.cpp вызывает avlInsert, avlSize, avlHeight.

Пример ввода-вывода 4.10.01

Input format: n followed by n integer keys for AVL insertions.
3
3 2 1
Output:
Nodes: 3
Height: 2
Rotations: 1

После LL-поворота: корень 2, слева 1, справа 3.

4.10 · Код · 4.10.03

Алгоритм: minHeap.cpp

lib/treeAlgs/minHeap.cpp

heapInit(h, buffer, cap):
  h.data = buffer; h.size = 0; h.capacity = cap

siftUp(h, i):  // вспомогательно; 4.10.03 напрямую push не вызывает
  while i > 0:
    p = (i - 1) / 2
    if h.data[p] <= h.data[i]: break
    swap(h.data[p], h.data[i])
    i = p

siftDown(h, i):
  loop:
    l = 2*i + 1; r = l + 1
    smallest = i
    if l < h.size and h.data[l] < h.data[smallest]: smallest = l
    if r < h.size and h.data[r] < h.data[smallest]: smallest = r
    if smallest == i: break
    swap(h.data[i], h.data[smallest]); i = smallest
heapBuild(h, keys, n):
  if !h or !h.data or n < 0 or n > h.capacity: return false
  скопировать keys[0..n-1] в h.data
  h.size = n
  for i = n/2 - 1 .. 0:    // снизу вверх
    siftDown(h, i)
  return true

heapExtractMin(h, valueOut):
  if !h or h.size <= 0 or !valueOut: return false
  *valueOut = h.data[0]
  h.data[0] = h.data[h.size - 1]
  --h.size
  if h.size > 0: siftDown(h, 0)
  return true

task41003.cpp: heapInit, heapBuild, затем q раз heapExtractMin → строки Extract: k. siftUp полезен для понимания push, но в task-файле не вызывается.

Пример ввода-вывода 4.10.03

Input format: n followed by n heap keys, then q extract-min operations.
6
5 3 8 1 9 2
2
Output:
Heap built: YES
Extract: 1
Extract: 2
4.10 · Код · 4.10.02/04

Алгоритм: rbTree.cpp

lib/treeAlgs/rbTree.cpp — используется в 4.10.02 и 4.10.04

// Утилиты (как в AVL/BST)
rbHeight, rbSize, rbInorder, rbFree

// bstInsert с parent-ссылками
if !root: inserted = new RbNode(key); return inserted
if key < root: left = bstInsert(left,...);
                left.parent = root
else if key > root: right = bstInsert(...);
                     right.parent = root
else: inserted = nullptr   // дубликат

// rotateLeft/Right(root, x, m):
++m.rotations; переставить x,y и parent;
обновить parent у детей и у корня

rbInsert(root, key, metrics):
  root = bstInsert(root, key, node)
  if !node: return root
  if node == root: node.color = BLACK; return root
  fixInsert(root, node, metrics)
  return root
fixInsert(root, node, m):
  while parent красный:
    grand = parent.parent
    uncle = (parent == grand.left)
              ? grand.right : grand.left

    if uncle красный:
      m.recolorings += 3
      parent, uncle = BLACK; grand = RED
      node = grand; continue вверх

    // дядя черный — два подслучая (L/R)
    если «ломаная» цепочка:
      поворот у parent → выровнять
    перекрасить parent/ grand
    поворот у grand
    break
  root.color = BLACK

Новый узел — красный (конструктор). Счетчики metrics.rotations и metrics.recolorings увеличивайте в поворотах и перекрасках.

Пример ввода-вывода 4.10.02

Input format: n followed by n integer keys for RB-tree insertions.
5
1 2 3 4 5
Output:
Nodes: 5
Height: 3
Rotations: 2
Recolorings: 5
Sorted inorder: YES

Пример ввода-вывода 4.10.04

Input format: n followed by n tree keys, then q followed by q search keys.
5
1 2 3 4 5
3
3
4
5
Output:
Height BST: 5
Height RB: 3
Total comparisons BST: 12
Total comparisons RB: 8
4.10 · Код

Задания 4.10 и файлы draft

ЗаданиеДописатьУже готово
4.10.01 AVLavlTree.cpp: повороты, rebalance, avlInsert, avlSize, avlInorder, avlFreetask41001.cpp
4.10.02 RBrbTree.cpp: утилиты, повороты, fixInsert, rbInserttask41002.cpp
4.10.03 HeapminHeap.cpp: heapInit, siftUp, siftDown, heapBuild, heapExtractMintask41003.cpp
4.10.04 Сравнениетот же rbTree.cpp: rbInsert, rbHeight, rbFreetask41004.cpp

Рекомендуемый порядок реализации

minHeap.cppavlTree.cpprbTree.cpp

4.10 · Код

Структура draft 4.10

seminars/4.10/draft/c++/
  lib/treeAlgs/
    avlTree.cpp    ← дописать
    rbTree.cpp     ← дописать
    minHeap.cpp    ← дописать
    binaryTree.cpp ← с 4.09
  src/task_4.10.NN/
    task410NN.cpp  ← готово
  test/...
  01.build.sh
  03.run_tests.sh

Порядок сложности

  1. minHeap.cpp — массив, без указателей
  2. avlTree.cpp — повороты, высоты
  3. rbTree.cpp — самый объемный fixup

Не копируйте reference целиком — разберите каждый случай.

4.10 · Сборка

Сборка и отладка

cd seminars/4.10/draft/c++
./01.build.sh
./03.run_tests.sh

Чеклист отладки RB

  • inorder отсортирован?
  • корень черный?
  • нет двух красных подряд?
  • после поворота AVL — высоты обновлены?
  • avlFree / rbFree вызваны?
4.10 · Инструменты

Отладка: тесты и чеклист

cd seminars/4.10/draft/c++
./01.build.sh
./03.run_tests.sh
  • AVL: inorder отсортирован?
  • AVL: |bf| ≤ 1 везде?
  • RB: корень черный?
  • RB: нет двух красных подряд?
  • Heap: каждый parent ≤ children?

Скрипты

01.build.sh — cmake + сборка

03.run_tests.sh — ctest с выводом ошибки

Windows: .bat вместо .sh

4.10 · Ошибки

Типичные ошибки

  • ! Путают кучу и БДП
  • ! После поворота AVL не обновляют высоты
  • ! Перепутаны случаи LR и RL
  • ! В RB fixup не обрабатывают дядю (uncle)
  • ! В куче путают sift up и sift down
  • ! Сравнивают BST и RB на разных ключах
  • ! Копируют rbTree.cpp целиком без понимания
  • ! Забывают освобождение памяти
4.10 · Практика

Порядок на паре (80 мин)

БлокВремяСодержание
Повтор + теория~15 минВырождение BST, AVL, RB, куча
Практика~38 минminHeap → AVL → RB
Сравнение~12 минBST vs RB, задание 4.10.04
Итоги~15 минЧеклист перед лаб. 4.03
  1. minHeap.cpp → 4.10.03 · ~10 мин
  2. avlTree.cpp → 4.10.01 · ~12 мин
  3. rbTree.cpp → 4.10.02 · ~15 мин
  4. task41004 + 03.run_tests · ~8 мин
4.10 · Итоги

Итоги курса по деревьям

Тема3.094.094.10
Термины, БДПтеориякодиспользуем
3 обхода + level-orderтеориякод (bin*)
Дерево из массивакод
Метрикиh, сравнения+ повороты, перекраски
AVL / RB / кучакод
  • Объяснить поворот и 4 случая AVL
  • Правила RB + fixup
  • Куча: sift up / sift down
  • Сравнить BST и RB на одних ключах
  • draft 4.09 + 4.10, 03.run_tests зеленый

Домашнее

Закрыть draft/c++ для 4.09 и 4.10.

Ориентир: ~10–12 часов на оба семинара.

Спасибо!

Семинар 4.10 · Вопросы?

seminars/4.10/draft/c++

Слайд: --:-- 1 / 1