Алгоритмизация и программирование · 1 курс · 80 мин · после 4.09
БДП при вставке 1,2,3,4,5 превращается в цепочку. Поиск O(n) — нет выигрыша перед массивом.
Цель: h = O(log n) → поиск и вставка ≈ log₂ n шагов даже на «плохих» данных.
| n (ключей) | h в цепочке BST | h ≈ log₂ n | Выигрыш |
|---|---|---|---|
| 8 | 8 | ≈ 3 | в 2–3 раза |
| 1 000 | 1 000 | ≈ 10 | ~100× |
| 1 000 000 | 1 000 000 | ≈ 20 | ~50 000× |
log₂ n — сколько раз можно делить n пополам, пока не дойдем до 1. Сбалансированное дерево «режет» задачу примерно пополам на каждом шаге.
| Термин | Что это такое | Зачем на семинаре |
|---|---|---|
| Вырождение БДП | При вставке ключей по возрастанию дерево становится цепочкой (каждый узел — один ребенок). Высота 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() |
| Термин | Что это такое | Зачем на семинаре |
|---|---|---|
| RB-tree | Red-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 |
YES/NO1,2,…,n → цепочкаBST при вставке 1,2,3,4,5:
h = O(log n) при любых вставках
Средство: повороты (+ перекраски в RB)
Inorder не меняется!
h = 5
БДП + у каждого узла |bf| ≤ 1. Высота O(log n). Задание 4.10.01.
До
После
Поворот влево — зеркально. Inorder сохраняется.
| Шаг | Действие |
|---|---|
| 1 | x = y.left, t = x.right |
| 2 | x.right = y |
| 3 | y.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;
| Шаг | Действие |
|---|---|
| 1 | y = x.right, t = y.left |
| 2 | y.left = x |
| 3 | x.right = t |
| 4 | Обновить высоты; вернуть y |
Inorder: x → t → y → z. Случай RR.
bf(v) = height(left) − height(right)
| bf | Смысл | Действие |
|---|---|---|
| −1…+1 | норма | — |
| +2 | левое на 2 выше | balanceFactor(node->left) → LL/LR |
| −2 | правое на 2 выше | balanceFactor(node->right) → RR/RL |
| Случай | Условие | Повороты |
|---|---|---|
| LL | bf=+2, bf(left)≥0 | rotateRight |
| RR | bf=−2, bf(right)≤0 | rotateLeft |
| LR | bf=+2, bf(left)<0 | rotateLeft(left), rotateRight |
| RL | bf=−2, bf(right)>0 | rotateRight(right), rotateLeft |
bf(3)=+2, bf(2)≥0 → rotateRight(3). Rotations: 1
bf(1)=−2, bf(2)≤0 → rotateLeft(1). Rotations: 1
rotateLeft(1), затем rotateRight(3). Rotations: 2
rotateRight(3), затем rotateLeft(1). Rotations: 2
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)
Формула bf работает только если высоты актуальны. Порядок после вставки:
updateHeightbf|bf| = 2 — поворот по таблице случаевupdateHeight у затронутых узловЛист без детей: height = 1, bf = 0. Пустой сын в формуле дает вклад 0.
+2 — тяжело слева, не справаbf=+2 смотрят правого сына вместо левогоЗабыли updateHeight — тесты падают с «не тем» числом поворотов.
Красно-черное дерево (Guibas, Sedgewick) — самобалансирующееся БДП. Баланс мягче, чем у AVL, зато вставки и удаления обычно дешевле.
| Свойство | Содержание |
|---|---|
| Как БДП | Порядок ключей: left < root < right, inorder отсортирован |
| Дополнительно | У каждого узла цвет: красный или черный |
| Высота | ≤ 2·log₂(n+1) — чуть выше AVL, но все равно O(log n) |
| Восстановление | Перекраска и/или поворот после вставки (fixInsert) |
| Где встречается | std::map, std::set (GCC), Java TreeMap |
bf у каждого узла; RB — за цветами на путяхparent-ссылки для поворотовЗадания 4.10.02 и 4.10.04 — rbTree.cpp.
Новый узел ставится как в BST и красится в красный, потому что красный узел не увеличивает черную высоту пути.
| Цвет нового узла | Что ломается |
|---|---|
| черный | Один путь получает лишний черный узел |
| красный | Может появиться только нарушение «красный → красный» |
| Ситуация | Цвета | Метрика |
|---|---|---|
| первый узел | root: red → black | не считаем отдельно |
| родитель черный | ничего | +0 |
| дядя красный | parent, uncle → black; grand → red | +3 |
| дядя черный, прямая цепочка | parent → black; grand → red | +1 в reference |
Вставили красный узел, родитель и дядя тоже красные: перекрашиваем родителя и дядю в черный, дедушку в красный; корень в конце снова черный.
Recolorings += 3, Rotations += 0.
Пример 1, 2, 3: перед исправлением цепочка 1(B) → 2(R) → 3(R). После rotateLeft(1): 2(B) с красными детьми 1 и 3.
Rotations += 1, Recolorings += 1 в этой реализации.
fixInsert поднимается вверх, пока нарушение не исчезнет| Ситуация | Действие |
|---|---|
| Родитель черный | Нарушений нет — стоп |
| Родитель красный, дядя красный | Перекрасить родителя, дядю → черные; дедушку → красный; подняться к дедушке |
| Родитель красный, дядя черный (или нет) | «Ломаная» цепочка → поворот у родителя; «прямая» → поворот у дедушки + перекраска |
Дядя — брат родителя (левый/правый сын дедушки). Считаем перекраски в Recolorings.
| AVL | Red-Black | |
|---|---|---|
| Жесткость баланса | Выше (|bf|≤1) | Мягче |
| Высота | Обычно меньше | Чуть больше, но O(log n) |
| Вставка/удаление | Часто больше поворотов | Обычно дешевле |
| Типичное применение | Частый поиск | Частые вставки/удаления |
| Главный инструмент | bf + повороты | цвета + fixInsert |
Таблица инвариантов, хранения, сложности поиска и минимума.
Куча — complete-бинарное дерево, хранящееся в массиве: все уровни заполнены, а последний заполняется слева направо. Главное правило — не порядок «слева меньше», а отношение родитель ↔ дети.
| Тип | Инвариант | Корень |
|---|---|---|
| Min-heap | родитель ≤ оба ребенка | минимум всего набора |
| Max-heap | родитель ≥ оба ребенка | максимум |
На семинаре — только min-heap (minHeap.cpp).
data[0] за O(1)Применение: очередь с приоритетом, алгоритм Дейкстры, пирамидальная сортировка.
| Операция | Алгоритм | Время |
|---|---|---|
heapBuild | Скопировать ключи, siftDown с n/2−1 до 0 | O(n) |
heapExtractMin | Взять data[0], последний в корень, siftDown(0) | O(log n) |
| Просмотр минимума | data[0] без извлечения | O(1) |
Задание 4.10.03: реализуем heapBuild и heapExtractMin. Heap built: YES печатается task-файлом после успешного heapBuild.
Новый элемент в конец массива. Пока он меньше родителя — меняем местами с родителем и идем вверх.
Индекс родителя: (i-1)/2
Корень (минимум) забираем. Последний элемент ставим в корень. Пока он больше меньшего ребенка — меняем именно с меньшим ребенком.
Дети: 2i+1, 2i+2
Массив [1,3,2,6,5,4] → extract → 1, затем extract → 2 (минимумы не убывают)
Вставка 1 2 3 4 5, затем q поисков — задание 4.10.04
Height BST: 5
Height RB: ≤ 3
Формат полного ввода-вывода для 4.10.04 теперь показан на слайде «Алгоритм: rbTree.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.
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.
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-файле не вызывается.
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
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 увеличивайте в поворотах и перекрасках.
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
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.01 AVL | avlTree.cpp: повороты, rebalance, avlInsert, avlSize, avlInorder, avlFree | task41001.cpp |
| 4.10.02 RB | rbTree.cpp: утилиты, повороты, fixInsert, rbInsert | task41002.cpp |
| 4.10.03 Heap | minHeap.cpp: heapInit, siftUp, siftDown, heapBuild, heapExtractMin | task41003.cpp |
| 4.10.04 Сравнение | тот же rbTree.cpp: rbInsert, rbHeight, rbFree | task41004.cpp |
minHeap.cpp → avlTree.cpp → rbTree.cpp
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
minHeap.cpp — массив, без указателейavlTree.cpp — повороты, высотыrbTree.cpp — самый объемный fixupНе копируйте reference целиком — разберите каждый случай.
cd seminars/4.10/draft/c++ ./01.build.sh ./03.run_tests.sh
avlFree / rbFree вызваны?cd seminars/4.10/draft/c++ ./01.build.sh ./03.run_tests.sh
01.build.sh — cmake + сборка
03.run_tests.sh — ctest с выводом ошибки
Windows: .bat вместо .sh
rbTree.cpp целиком без понимания| Блок | Время | Содержание |
|---|---|---|
| Повтор + теория | ~15 мин | Вырождение BST, AVL, RB, куча |
| Практика | ~38 мин | minHeap → AVL → RB |
| Сравнение | ~12 мин | BST vs RB, задание 4.10.04 |
| Итоги | ~15 мин | Чеклист перед лаб. 4.03 |
03.run_tests · ~8 мин| Тема | 3.09 | 4.09 | 4.10 |
|---|---|---|---|
| Термины, БДП | теория | код | используем |
| 3 обхода + level-order | теория | код (bin*) | — |
| Дерево из массива | — | код | — |
| Метрики | — | h, сравнения | + повороты, перекраски |
| AVL / RB / куча | — | — | код |
03.run_tests зеленыйЗакрыть draft/c++ для 4.09 и 4.10.
Ориентир: ~10–12 часов на оба семинара.
seminars/4.10/draft/c++