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

Бинарные деревья и BST

Подробный разбор для 1 курса — от терминов до кода

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

4.09 · План курса

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

3.09Теория БДП, 3 обхода, рекурсия, GTest
4.09Код: массив→дерево, 4 обхода, метрики
4.10AVL, RB-tree, куча
Тема3.094.09 (сегодня)
Обходы3 рекурсивных (теория)NEW + level-order в коде
Построениевставка в БДПNEW по массиву, -1
МетрикиNEW высота, сравнения
БДП insert/findтеорияготово в tree.cpp
4.09 · Введение

Что мы делаем сегодня

Цель семинара

На 3.09 вы узнали, что такое бинарное дерево и БДП. Сегодня учимся писать это на C++ и проверять тестами.

  • Построить дерево из массива
  • Реализовать 4 обхода
  • Работать с БДП: проверка, поиск, высота

Формат как на прошлых семинарах

  1. Открываете draft/c++
  2. Дописываете функции в .cpp
  3. ./01.build.sh./03.run_tests.sh
  4. Чините по имени упавшего теста

Семинар 80 мин: ~18 теория + ~35 практика + ~12 разбор + ~15 итоги.

4.09 · Словарь

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

ТерминЧто это такоеЗачем на семинаре
ДеревоСвязная структура без циклов: есть корень, у узла — родитель и дети, от корня до любого узла путь один.Базовая модель для всех заданий
Бинарное деревоУ каждого узла не более двух детей: left и right. Порядок ключей не задан.Задание 4.09.01 — строим из массива
БДП (BST)Binary Search Tree — бинарное дерево поиска: в каждом узле все ключи слева меньше, справа больше ключа узла.Задания 4.09.02–04 — вставка, поиск, высота
КлючЧисло (значение), хранящееся в узле. В коде — поле key.Сравниваем при поиске и вставке
УказательСсылка на другой узел в памяти (BinNode*). Если ребенка нет — nullptr.Связываем узлы в дерево
ИнвариантПравило, которое структура обязана соблюдать всегда. У БДП: left < root < right в каждом узле.Проверяем через Sorted inorder: YES/NO
Level-order (по уровням)Способ задать форму дерева массивом: корень в [0], дети узла i — в 2i+1 и 2i+2; -1 = пусто.buildByLevelOrder — это не вставка в БДП!
4.09 · Словарь

Словарь терминов — обходы и метрики

ТерминЧто это такоеЗачем на семинаре
Обход дереваСпособ посетить все узлы и вывести ключи в определенном порядке.4.09.01 — четыре вида обхода
PreorderПорядок: корень → левое → правое. Сначала обрабатываем узел, потом детей.binPreorder, рекурсия
InorderПорядок: левое → корень → правое. В БДП дает ключи в отсортированном порядке.Проверка Sorted inorder
PostorderПорядок: левое → правое → корень. Сначала дети, потом узел.Удобен при освобождении памяти (binFree)
Level-order обходУровень за уровнем, слева направо. Реализуется очередью (как BFS по детям).binLevelOrder — новый обход в коде
Высота hДлина самого длинного пути от корня до листа. В draft: пустое дерево → 0, один узел → 1.4.09.04 — сравниваем два порядка вставки
Сравнение (comparison)Одно сравнение искомого ключа с ключом текущего узла при поиске в БДП (+1 на каждый шаг).4.09.03 — ключ: found=… comparisons=…
GTestБиблиотека автотестов для C++: TEST, EXPECT_EQ и др. Проверяет ваш код без ручного ввода.03.run_tests.sh запускает все тесты
4.09 · Повтор

Что вы уже знаете 3.09

  • Корень, лист, родитель, потомок
  • БДП: left < root < right
  • Preorder, inorder, postorder
  • Вставка и поиск в БДП
  • Рекурсия и стек вызовов
  • GTest: TEST, EXPECT_*
  • Формат: taskNN.hpp/cpp
  • Библиотека lib/ioutils/trees/

Мини-шпаргалка

Preorder — «сначала корень» (удобно копировать дерево). Inorder — «слева, потом корень» (дает сортировку в БДП). Postorder — «сначала дети» (удобно удалять).

Ниже повторим определения подробнее — даже если 3.09 был давно.

4.09 · Теория

Дерево: термины для 1 курса

ТерминЗначениеНа рисунке →
Узел (node)Круг с ключом и ссылками на детейлюбой круг
Корень (root)Верхний узел, с него начинаем5 (оранжевый)
РебенокУзел на уровень ниже3 и 7 у 5
ЛистНет детей (left и right пусты)1 и 4
ПоддеревоУзел + все потомкилевое от 5: {3,1,4}
Высота hСамый длинный путь корень → листздесь h = 3 (в draft)

БДП: вставка 5, 3, 7, 1, 4

5 3 7 1 4
Корень — 5
Дети корня — 3 и 7
Листья — 1 и 4 (у 7 тоже нет детей)
━━Левое поддерево корня — узлы 3, 1, 4
Высота h = 3 (путь 5 → 3 → 1)

Бинарное дерево в коде

Не больше двух детей: left и right (если нет ребенка — nullptr).

struct BinNode { int key; BinNode *left; BinNode *right; };
4.09 · Теория

БДП (BST) — определение

BST = Binary Search Tree = бинарное дерево поиска (БДП). Структура для хранения ключей с быстрым поиском: на каждом шаге отбрасываем половину вариантов.

Инвариант БДП: для каждого узла v все ключи слева меньше v.key, все справа — больше.
5 3 7 1 4

Проверка для узла 5:

  • Слева: 3, 1, 4 — все < 5 ✓
  • Справа: 7 — больше 5 ✓

Inorder (лево → корень → право):

1 3 4 5 7 — отсортировано!

Поэтому в задании 4.09.02 выводим Sorted inorder: YES.

4.09 · Теория

Вставка в БДП — пошагово

Ключи вставляем по очереди: 5 → 3 → 7 → 1 → 4

ШагКлючДействиеДерево после
15Пусто → 5 становится корнем5
233 < 5 → влево5 / 3
377 > 5 → вправо5 / 3,7
411 < 5 → влево; 1 < 3 → влево от 3лист слева от 3
544 < 5 → влево; 4 > 3 → вправо от 3готовое дерево

Алгоритм: начинаем с корня; если ключ меньше — в left, иначе в right; если ребенка нет — создаем новый узел. Сложность одной вставки: O(h), где h — высота.

4.09 · Теория

Сложность: что значит O(h)

Простыми словами

h — высота дерева (сколько «ступенек» от корня до самого дальнего листа).

Поиск и вставка в БДП каждый раз выбирают левую или правую ветвь → проходим путь длиной не больше h.

  • Если h маленькая → быстро
  • Если h = n (цепочка) → как линейный список, O(n)
ОперацияВремяКогда хорошо
ПоискO(h)h ≈ log₂ n
ВставкаO(h)случайный порядок ключей
Обход всех узловO(n)всегда посещаем каждый узел
Плохой случайO(n)вставка 1,2,3,…,n по порядку
4.09 · Новое

Что нового на 4.09

Было на 3.09Новое сейчас
3 обхода (теория)NEW 4-й обход level-order в коде
Вставка в БДПNEW Построение по массиву (-1 = пусто)
NEW Обычное бинарное дерево ≠ БДП
NEW Метрики: высота, число сравнений
NEW Sorted inorder: YES/NO
Задания 4.09.01–04 в draft/c++
4.09 · Важно

Обычное дерево ≠ БДП

Обычное бинарное дерево (4.09.01)БДП (4.09.02–04)
Как строимМассив level-order, индексы 2i+1, 2i+2Вставка ключей по одному
Порядок ключейМожет нарушать left < root < rightВсегда соблюдается
ФайлbinaryTree.cpp, BinNodetree.cpp, BstNode
InorderНе обязан быть отсортированДолжен быть отсортирован
В задании 4.09.01 нельзя вызывать bstInsert — только buildByLevelOrder!
4.09 · Контекст

Разные «деревья» в курсе

КонтекстСеминарСуть
Бинарное дерево / БДП3.09, 4.09Упорядоченная структура, insert/find
DFS-дерево обхода4.04, 4.05Подграф обхода графа
Лес DSU4.07Множество корней, find/union
Остовное дерево (MST)4.07Связный подграф, n−1 ребер
Сегодня работаем только с бинарными деревьями как структурой данных.
4.09 · Повтор

БДП и три обхода 3.09

5 3 7 1 4
ОбходПорядокРезультат
Preorderкорень → лево → право5 3 1 4 7
Inorderлево → корень → право1 3 4 5 7
Postorderлево → право → корень1 4 3 7 5

Inorder БДП → отсортированная последовательность. Это главная проверка корректности.

4.09 · Теория

Рекурсия в обходах

void binPreorder(node): if node == nullptr: return записать node.key // корень binPreorder(node.left) // левое binPreorder(node.right) // правое

Как это работает

  1. База: пустой узел — ничего не делаем
  2. Обрабатываем текущий узел (выводим ключ)
  3. Рекурсивно вызываем для левого поддерева
  4. Рекурсивно вызываем для правого поддерева

Inorder и postorder — те же три шага, но в другом порядке (см. таблицу на след. слайдах).

В tree.cpp уже есть bstPreOrder — скопируйте логику, заменив тип узла на BinNode.

4.09 · Новое

Построение по уровням из массива

Это НЕ вставка в БДП! Форма задается индексами массива.
Индекс: 0 1 2 3 4 5 6 Значение: 10 5 15 3 7 -1 20 Дети узла i: left = 2*i + 1 right = 2*i + 2 Пустой узел: keys[i] == -1
ур.0 ур.1 ур.2 10 5 15 3 7 20
4.09 · Алгоритм

buildByLevelOrder — идея

build(i): if i >= n or keys[i] == -1: return nullptr node = new BinNode(keys[i]) node.left = build(2*i + 1) node.right = build(2*i + 2) return node корень = build(0)

Пример: массив из 7 элементов

i:     0   1   2   3  4  5   6
key:  10   5  15   3  7 -1  20
  • Индекс 0 → корень 10
  • Дети 10: индексы 1 и 2 → 5 и 15
  • Индекс 5 = -1 → нет левого ребенка у 15
  • Индекс 6 → правый ребенок 15 = 20

Так же устроен массив в куче (семинар 4.10), но там другой смысл ключей.

4.09 · Новое

Четыре обхода

ОбходПорядокРезультатРеализация
Preorderкорень → лево → право10 5 3 7 15 20рекурсия
Inorderлево → корень → право3 5 7 10 15 20рекурсия
Postorderлево → право → корень3 7 5 20 15 10рекурсия
Level-orderпо уровням, слева направо10 5 15 3 7 20очередь
queue.push(root) while (!queue.empty()): node = queue.front() queue.pop() вывести node.key if node.left → queue.push(left) if node.right → queue.push(right)

Функции: binPreorder, binInorder, binPostorder, binLevelOrder

4.09 · Алгоритм

Level-order: зачем очередь

Нужно вывести узлы уровень за уровнем, слева направо: сначала корень, потом его дети, потом внуки…

Порядок обработки для дерева 10,5,15,3,7

105, 153, 7

Очередь хранит «кого обработать следующим».

queue.push(root) while queue не пуста: node = queue.front() // FIFO! queue.pop() вывести node.key if node.left: queue.push(left) if node.right: queue.push(right)

В C++ используйте std::queue<BinNode*> или массив с индексами head/tail.

4.09 · Новое · 4.09.04

Метрика: высота БДП

Один набор ключей {3, 1, 4, 2, 5} — два порядка вставки, разная высота:

Исходный порядок: 3, 1, 4, 2, 5

3 1 4 2 5

Height original: 3

Отсортированный порядок: 1, 2, 3, 4, 5

1 2 3 4 5

Height sorted: 5 → поиск O(n)

Высота считается в узлах: пустое дерево → 0, один узел → 1. Далее — пошаговый поиск ключа 4 в отдельном БДП.

4.09 · Теория

Высота дерева — определение

В draft 4.09

  • nullptr (пустое) → высота 0
  • Один узел без детей → высота 1
  • Иначе: 1 + max(h(left), h(right))

Зачем в 4.09.04 два порядка

Один и тот же набор ключей {3,1,4,2,5}:

  • Исходный порядок → дерево «похожее на сбалансированное», h = 3
  • Отсортированный 1,2,3,4,5 → цепочка, h = 5

Сортировку копии делаем вставками (как в задании), не std::sort.

4.09 · Поиск · старт

Поиск в БДП: ключ 4

5 3 7 1 4 корень
Подготовка

БДП построен вставкой ключей: 5 → 3 → 7 → 1 → 4

Запрос

Найти ключ 4

Старт — в корне дерева.

Правило подсчета

На каждом шаге: +1 comparison при сравнении запроса с ключом узла.

→ Следующий слайд: шаг 1

4.09 · Поиск · шаг 1/3

Шаг 1: сравнение с корнем

5 3 7 1 4
Шаг 1 из 3
4 < 5 ?  →  да

Идем в левое поддерево (к узлу 3).

comparisons = 1

Узел 7 не посещаем — он в правой ветви.

4.09 · Поиск · шаг 2/3

Шаг 2: сравнение с узлом 3

5 3 7 1 4
Шаг 2 из 3

Уже посетили: 5

4 > 3 ?  →  да

Идем в правое поддерево (к узлу 4).

comparisons = 2

Узел 1 не посещаем — он слева от 3.

4.09 · Поиск · шаг 3/3

Шаг 3: ключ найден

5 3 7 1 4
Шаг 3 из 3

Путь: 5 → 3 → 4

4 == 4 ?  →  найдено!
comparisons = 3

4: found=YES comparisons=3

4.09 · Поиск · итог

Итог: метрики поиска (4.09.03)

5 3 7 1 4

━━ путь поиска   текущий шаг   найден

ШагСравнениеДействие
14 vs 5влево
24 vs 3вправо
34 vs 4найдено
Output:
4: found=YES comparisons=3
Total comparisons: 3   (для одного запроса)

Задание 4.09.02: Sorted inorder: YES для этого БДП (inorder: 1 3 4 5 7).

4.09 · Поиск

Поиск: ключ не найден

5 7 3 1 4

Ищем ключ 6 в том же БДП:

ШагСравнениеКуда
16 > 5вправо → 7
26 < 7влево, но детей нет → стоп
Output:
6: found=NO comparisons=2

Сравнение на последнем узле тоже считается! Не забывайте +1 при равенстве (found=YES).

4.09 · Практика

Задания 4.09.01–04

ЗадачаВводВывод
4.09.01Дерево по уровням, 4 обходаn, массив (-1)Preorder:Level-order:
4.09.02БДП + inordern, ключиInorder:Sorted inorder: YES/NO
4.09.03Поиск с метрикойключи + q запросовключ: found=… comparisons=…, Total comparisons:
4.09.04Два порядка вставкиn, ключиHeight original: Height sorted:

Код: seminars/4.09/draft/c++ · Постановки: seminar409Tasks.md

4.09 · Код · 4.09.01

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

lib/treeAlgs/binaryTree.cpp — дописать построение, обходы и освобождение памяти

buildByLevelOrder(keys, n):
  if n <= 0: return nullptr
  return buildByIndex(keys, n, 0)

buildByIndex(keys, n, i):
  if i >= n: return nullptr
  if keys[i] == -1: return nullptr

  node = new BinNode(keys[i])
  node.left  = buildByIndex(keys, n, 2*i + 1)
  node.right = buildByIndex(keys, n, 2*i + 2)
  return node
binPreorder(node, out):
  if !node: return
  push node.key → left → right

binInorder(node, out):
  if !node: return
  left → push node.key → right

binPostorder(node, out):
  left → right → push node.key

binLevelOrder(root, out):
  queue.push(root)
  while !queue.empty():
    cur = queue.front(); queue.pop()
    push cur.key
    if cur.left: queue.push(cur.left)
    if cur.right: queue.push(cur.right)

binFree(root):
  left → right → delete root

-1 означает пустой узел в массиве. Индексы детей в level-order массиве: 2*i+1 и 2*i+2.

4.09 · Код · 4.09.01

Алгоритм: runLevelOrderTreeTask

task_4.09.01/task40901.cpp

runLevelOrderTreeTask(keys, n, output):
  if !output or n < 0: return 1

  root = buildByLevelOrder(keys, n)

  preorder = []
  inorder = []
  postorder = []
  levelorder = []

  binPreorder(root, preorder)
  binInorder(root, inorder)
  binPostorder(root, postorder)
  binLevelOrder(root, levelorder)

  output << formatTraversalLine("Preorder", preorder)
  output << formatTraversalLine("Inorder", inorder)
  output << formatTraversalLine("Postorder", postorder)
  output << formatTraversalLine("Level-order", levelorder)

  binFree(root)
  return 0

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

Input format: n followed by n integer keys (-1 means an empty node).
7
10 5 15 3 7 -1 20
Output:
Preorder: 10 5 3 7 15 20
Inorder: 3 5 7 10 15 20
Postorder: 3 7 5 20 15 10
Level-order: 10 5 15 3 7 20

formatTraversalLine уже готова: не меняйте формат подписей и пробелов.

4.09 · Код · 4.09.02

Алгоритм: buildBstAndCheckInorder

task_4.09.02/task40902.cpp

buildBstAndCheckInorder(keys, n, output):
  if !keys or !output or n < 0: return 1

  root = nullptr
  for i in 0..n-1:
    root = bstInsert(root, keys[i])

  values = []
  bstInOrder(root, values)

  output << "Inorder: " + join(values)

  if isStrictlyIncreasing(values):
    output << "Sorted inorder: YES"
  else:
    output << "Sorted inorder: NO"

  bstFree(root)
  return 0

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

Input format: n followed by n integer keys for the BST.
5
5 3 7 1 4
Output:
Inorder: 1 3 4 5 7
Sorted inorder: YES

Для корректной БДП симметричный обход идет по возрастанию. Если в данных или вставке нарушить инвариант, будет NO.

4.09 · Код · 4.09.03

Алгоритм: runBstSearchMetrics

task_4.09.03/task40903.cpp

runBstSearchMetrics(keys, n, queries, q, output):
  if bad pointers or n < 0 or q < 0: return 1

  root = nullptr
  for key in keys:
    root = bstInsert(root, key)

  total = 0
  for query in queries:
    comparisons = 0
    found = bstFindWithComparisons(root, query, &comparisons)
    total += comparisons

    output << query
           << ": found=" << (found ? "YES" : "NO")
           << " comparisons=" << comparisons

  output << "Total comparisons: " << total
  bstFree(root)
  return 0

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

Input format: n followed by n BST keys, then q followed by q search keys.
5
5 3 7 1 4
3
4
6
7
Output:
4: found=YES comparisons=3
6: found=NO comparisons=2
7: found=YES comparisons=2
Total comparisons: 7

Поиск 6: сравнили с 5, затем с 7, дальше нужного ребенка нет.

4.09 · Код · 4.09.04

Алгоритм: sortKeysCopy и bstHeightAfterInsertions

task_4.09.04/task40904.cpp

sortKeysCopy(src, n, dst):
  скопировать src[0..n-1] в dst
  for i = 1..n-1:
    x = dst[i]
    j = i - 1
    while j >= 0 and dst[j] > x:
      dst[j+1] = dst[j]
      --j
    dst[j+1] = x

bstHeightAfterInsertions(keys, n):
  root = nullptr
  for i = 0..n-1:
    root = bstInsert(root, keys[i])
  h = bstHeight(root)
  bstFree(root)
  return h
main/task:
  read n and keys

  sorted = new int[n]
  sortKeysCopy(keys, n, sorted)

  hOriginal = bstHeightAfterInsertions(keys, n)
  hSorted = bstHeightAfterInsertions(sorted, n)

  print "Height original: ..."
  print "Height sorted: ..."
  delete[] sorted

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

Input format: n followed by n integer keys for BST insertions.
5
3 1 4 2 5
Output:
Height original: 3
Height sorted: 5
4.09 · Код

Draft: что готово и что дописать

✓ Уже готово — не трогать

  • lib/ioutils/trees/tree.cpp — БДП
  • bstMetrics.cpp — поиск с сравнениями
  • formatTraversalLine в task40901
  • Все main.cpp, тесты GTest

✎ Дописать самим

  • lib/treeAlgs/binaryTree.cppядро
  • task40901.cpp — склейка обходов
  • task40902.cpp — БДП + YES/NO
  • task40903.cpp — сумма сравнений
  • task40904.cpp — сортировка вставками + высоты

binPreorder — та же логика, что bstPreOrder в tree.cpp, но для BinNode.

4.09 · Память

Освобождение памяти

void binFree(node): if !node: return binFree(node.left) binFree(node.right) delete node // или free в C

Каждый new BinNode в buildByLevelOrder нужно освободить через binFree.

Аналогично bstFree для БДП.

  • Вызывайте в конце run*Task
  • Не освобождайте дважды
  • После binFree(root) не используйте root
4.09 · Сборка

Сборка и тесты

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

Формат работы

  • Логика → task409NN.cpp
  • Ввод-вывод → main.cpp
  • Проверка → Google Test
  • Внутренние функции → static

После каждого задания

  1. Собрать проект
  2. Запустить ./03.run_tests.sh
  3. Читать имя упавшего теста
  4. Исправить → повторить

Что делают скрипты

  • 01.build.sh — создает build/, качает GTest (первый раз), компилирует
  • 03.run_tests.sh — проверяет наличие build/, запускает ctest --output-on-failure
4.09 · Инструменты

Как читать падение GTest

$ ./03.run_tests.sh
...
FAILED: task_4.09.01_tests
  Expected equality of these values:
    actual "Preorder: 10 5 ..."
    expected "Preorder: 10 5 3 7 15 20"

Что делать

  1. Имя теста → файл test_task_40901.cpp
  2. Сравните actual и expected
  3. Часто ошибка в порядке обхода или пропущенном узле
  4. Исправили → снова ./01.build.sh и ./03.run_tests.sh

03.run_tests.sh = ctest --output-on-failure из папки build/.

4.09 · Ошибки

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

  • ! Строят БДП вместо дерева по индексам
  • ! Путают inorder и preorder
  • ! Забывают binFree / bstFree
  • ! Не реализуют binLevelOrder
  • ! Считают сравнения только при found=NO
  • ! Путают высоту в ребрах и в узлах
  • ! В 4.09.04 используют std::sort вместо вставок
  • ! Не обрабатывают пустой корень (-1 в [0])
4.09 · Практика

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

БлокВремяСодержание
Теория~18 минТермины, БДП, обходы, построение по массиву
Практика~35 мин4 задания ниже
Разбор~12 минТипичные ошибки, метрики
Итоги~15 минЧеклист, мост к 4.10
  1. binaryTree.cpp — построение, 4 обхода, binFree · ~12 мин → 03.run_tests
  2. task40902.cpp — БДП + Sorted inorder · ~8 мин
  3. task40903.cpp — поиск + сравнения · ~8 мин
  4. task40904.cpp — две высоты · ~7 мин
4.09 · Итоги

Итоги 4.09

  • Четыре обхода — наизусть по определению
  • Построение по массиву с -1
  • БДП: inorder, YES/NO
  • Поиск с подсчетом сравнений
  • Объяснить вырождение БДП
  • 03.run_tests проходит на draft

Мост к 4.10

БДП при плохом порядке → h = n, поиск O(n).

Решение: AVL, RB-tree, куча — сбалансированные структуры.

Спасибо!

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

seminars/4.09/draft/c++

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