Деревья и обходы

⏱ 50 мин · 🧩 заданий: 5

Термины

Дерево — узлы, связанные «родитель — дети», без циклов, с одним корнем. Двоичное дерево — у каждого узла не больше двух детей: левый и правый.

8 ← корень (уровень 0) / \ 3 10 ← уровень 1 / \ \ 1 6 14 ← уровень 2 / \ / 4 7 13 ← листья: 1, 4, 7, 13 высота = 4 (узлов на самом длинном пути), у узла 6 поддерево {6, 4, 7}
Термин Значение
корень узел без родителя
лист узел без детей
глубина узла расстояние от корня
высота дерева длина самого длинного пути от корня до листа
поддерево узел со всеми потомками
сбалансированное высоты поддеревьев любого узла отличаются не больше чем на 1 → высота O(log n)

Деревья повсюду: файловая система, DOM в HTML, AST в компиляторе, индексы БД (B-деревья), JSON.

class TreeNode:
    def __init__(self, val, left=None, right=None):
        self.val, self.left, self.right = val, left, right

def build(values):
    """Из списка по уровням (как в LeetCode): None — пустое место."""
    if not values or values[0] is None:
        return None
    nodes = [TreeNode(v) if v is not None else None for v in values]
    kids = iter(nodes[1:])
    for node in nodes:
        if node:
            node.left, node.right = next(kids, None), next(kids, None)
    return nodes[0]

def show(node, depth=0):
    """Печать дерева, повёрнутого на 90°: корень слева, правые потомки сверху."""
    if node:
        show(node.right, depth + 1)
        print("      " * depth + str(node.val))
        show(node.left, depth + 1)

root = build([8, 3, 10, 1, 6, None, 14, None, None, 4, 7, 13])
show(root)

Обход в глубину (DFS)

Три порядка отличаются тем, когда обрабатывается сам узел:

class TreeNode:
    def __init__(self, val, left=None, right=None):
        self.val, self.left, self.right = val, left, right

root = TreeNode(1, TreeNode(2, TreeNode(4), TreeNode(5)), TreeNode(3))

def preorder(node):                 # узел → левое → правое: копирование, сериализация
    if not node:
        return []
    return [node.val] + preorder(node.left) + preorder(node.right)

def inorder(node):                  # левое → узел → правое: в BST — по возрастанию
    if not node:
        return []
    return inorder(node.left) + [node.val] + inorder(node.right)

def postorder(node):                # левое → правое → узел: удаление, вычисление размеров
    if not node:
        return []
    return postorder(node.left) + postorder(node.right) + [node.val]

print("pre: ", preorder(root))
print("in:  ", inorder(root))
print("post:", postorder(root))

Пройдите пример 👣 Пошагово: видно, как растёт и сворачивается стек рекурсии.

Итеративно со стеком

Рекурсия в Python ограничена ~1000 уровней. Для глубоких деревьев — явный стек:

class TreeNode:
    def __init__(self, val, left=None, right=None):
        self.val, self.left, self.right = val, left, right

root = TreeNode(1, TreeNode(2, TreeNode(4), TreeNode(5)), TreeNode(3))

def preorder_iter(root):
    result, stack = [], [root] if root else []
    while stack:
        node = stack.pop()
        result.append(node.val)
        if node.right:                  # правый кладём первым — достанем вторым
            stack.append(node.right)
        if node.left:
            stack.append(node.left)
    return result

def inorder_iter(root):
    result, stack, node = [], [], root
    while stack or node:
        while node:                     # спускаемся влево до упора
            stack.append(node)
            node = node.left
        node = stack.pop()
        result.append(node.val)
        node = node.right
    return result

print(preorder_iter(root), inorder_iter(root))

Обход в ширину (BFS)

По уровням, с очередью:

from collections import deque

class TreeNode:
    def __init__(self, val, left=None, right=None):
        self.val, self.left, self.right = val, left, right

root = TreeNode(1, TreeNode(2, TreeNode(4), TreeNode(5)), TreeNode(3, None, TreeNode(6)))

def level_order(root):
    if not root:
        return []
    result, queue = [], deque([root])
    while queue:
        level = []
        for _ in range(len(queue)):     # ровно столько узлов, сколько на текущем уровне
            node = queue.popleft()
            level.append(node.val)
            if node.left:
                queue.append(node.left)
            if node.right:
                queue.append(node.right)
        result.append(level)
    return result

levels = level_order(root)
print(levels)
print("вид справа:", [lvl[-1] for lvl in levels])
print("уровней:", len(levels), "| сумма по уровням:", [sum(lvl) for lvl in levels])

BFS нужен, когда важен уровень: минимальная глубина, вид справа, зигзаг, соединение соседей на уровне.

Рекурсивное мышление

Большинство задач на деревья решаются по схеме: «ответ для узла = функция(ответ для левого, ответ для правого)».

class TreeNode:
    def __init__(self, val, left=None, right=None):
        self.val, self.left, self.right = val, left, right

root = TreeNode(5, TreeNode(4, TreeNode(11, TreeNode(7), TreeNode(2))), TreeNode(8, TreeNode(13), TreeNode(4, None, TreeNode(1))))

def size(node):
    return 0 if not node else 1 + size(node.left) + size(node.right)

def height(node):
    return 0 if not node else 1 + max(height(node.left), height(node.right))

def mirror(node):
    if node:
        node.left, node.right = mirror(node.right), mirror(node.left)
    return node

def has_path_sum(node, target):
    if not node:
        return False
    if not node.left and not node.right:            # лист
        return node.val == target
    rest = target - node.val
    return has_path_sum(node.left, rest) or has_path_sum(node.right, rest)

def is_balanced(node):
    def check(n):                                   # высота или -1, если не сбалансировано
        if not n:
            return 0
        l, r = check(n.left), check(n.right)
        if l < 0 or r < 0 or abs(l - r) > 1:
            return -1
        return 1 + max(l, r)
    return check(node) >= 0

print(size(root), height(root), has_path_sum(root, 22), is_balanced(root))

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

Для каждого узла: всё слева меньше, всё справа больше. Поиск, вставка, удаление — O(h), где h — высота: O(log n) для сбалансированного дерева, O(n) для вырожденного («палки»).

class TreeNode:
    def __init__(self, val, left=None, right=None):
        self.val, self.left, self.right = val, left, right

def insert(root, val):
    if not root:
        return TreeNode(val)
    if val < root.val:
        root.left = insert(root.left, val)
    elif val > root.val:
        root.right = insert(root.right, val)
    return root

def search(root, val):
    node = root
    while node and node.val != val:
        node = node.left if val < node.val else node.right
    return node is not None

def inorder(node):
    return inorder(node.left) + [node.val] + inorder(node.right) if node else []

def kth_smallest(root, k):
    stack, node = [], root
    while True:
        while node:
            stack.append(node)
            node = node.left
        node = stack.pop()
        k -= 1
        if k == 0:
            return node.val
        node = node.right

root = None
for v in [8, 3, 10, 1, 6, 14, 4, 7, 13]:
    root = insert(root, v)
print(inorder(root))                  # симметричный обход BST — отсортированный порядок
print(search(root, 6), search(root, 5), kth_smallest(root, 3))

Сбалансированные деревья поиска (AVL, красно-чёрные) держат высоту O(log n) поворотами. В Python их нет в стандартной библиотеке — вместо них используют dict/set (хеш-таблица, без порядка), bisect по отсортированному списку или пакет sortedcontainers.

Префиксное дерево (trie)

Дерево, где каждый путь от корня — префикс слов. Автодополнение, проверка орфографии, поиск по префиксу за O(длины слова):

class Trie:
    def __init__(self):
        self.root = {}

    def insert(self, word):
        node = self.root
        for ch in word:
            node = node.setdefault(ch, {})
        node["$"] = True                         # конец слова

    def _find(self, prefix):
        node = self.root
        for ch in prefix:
            if ch not in node:
                return None
            node = node[ch]
        return node

    def contains(self, word):
        node = self._find(word)
        return bool(node and node.get("$"))

    def complete(self, prefix):
        node, result = self._find(prefix), []
        def walk(n, path):
            for ch, child in n.items():
                if ch == "$":
                    result.append(prefix + path)
                else:
                    walk(child, path + ch)
        if node:
            walk(node, "")
        return sorted(result)

t = Trie()
for w in ["кот", "котёнок", "кофе", "код", "дом"]:
    t.insert(w)
print(t.contains("кот"), t.contains("ко"), t.complete("ко"))

Подход на собеседовании

Уточните: двоичное ли дерево, BST ли, могут ли быть повторы, что вернуть для пустого дерева. Решение обычно — рекурсия «что вернуть из поддерева». Назовите сложность: O(n) по времени (каждый узел один раз), O(h) по памяти на стек рекурсии.

Итог

  • DFS: прямой, симметричный, обратный — рекурсией или стеком; BFS по уровням — очередью.
  • Задачи на деревья — рекурсия «ответ из ответов поддеревьев».
  • BST: слева меньше, справа больше; inorder даёт сортировку; операции O(h).
  • Проверка BST — через границы (low, high), а не сравнение с детьми.
  • Trie — поиск по префиксу.

Практика решено 0 из 5

Вы не вошли — ответы проверяются, но прогресс не сохранится. Войти или зарегистрироваться.
Задание 1 Программа на Python

Высота дерева. Верните максимальную глубину двоичного дерева (число узлов на самом длинном пути от корня до листа). Пустое дерево — 0. Класс TreeNode и build (из списка по уровням) уже есть.

Какие тесты проверяют код (2)
assert max_depth(build([3, 9, 20, None, None, 15, 7])) == 3
assert max_depth(None) == 0 and max_depth(build([1])) == 1 and max_depth(build([1, None, 2])) == 2
Задание 2 Программа на Python

Обход по уровням. Верните значения узлов по уровням: список списков. [3, 9, 20, None, None, 15, 7] → [[3], [9, 20], [15, 7]]. Используйте очередь deque (BFS).

Какие тесты проверяют код (2)
assert level_order(build([3, 9, 20, None, None, 15, 7])) == [[3], [9, 20], [15, 7]]
assert level_order(None) == [] and level_order(build([1, 2, 3, 4, None, None, 5])) == [[1], [2, 3], [4, 5]]
Задание 3 Программа на Python

Корректное дерево поиска. Проверьте, является ли дерево двоичным деревом поиска (BST): все значения в левом поддереве строго меньше узла, в правом — строго больше, и так для каждого узла. Осторожно: сравнивать только с непосредственными детьми недостаточно.

Какие тесты проверяют код (3)
assert is_valid_bst(build([2, 1, 3])) and not is_valid_bst(build([5, 1, 4, None, None, 3, 6]))
assert not is_valid_bst(build([5, 4, 6, None, None, 3, 7])), "3 в правом поддереве 5 — нарушение, хотя 3 < 6 корректно для своего родителя"
assert is_valid_bst(None) and not is_valid_bst(build([2, 2]))
Задание 4 Программа на Python

Наименьший общий предок в BST. Даны корень двоичного дерева поиска и два значения p и q, которые есть в дереве. Верните значение их наименьшего общего предка. Используйте свойство BST — O(h).

Какие тесты проверяют код (2)
tree = build([6, 2, 8, 0, 4, 7, 9, None, None, 3, 5]); assert lca_bst(tree, 2, 8) == 6 and lca_bst(tree, 2, 4) == 2 and lca_bst(tree, 3, 5) == 4
tree = build([6, 2, 8, 0, 4, 7, 9, None, None, 3, 5]); assert lca_bst(tree, 0, 5) == 2 and lca_bst(tree, 7, 9) == 8
Задание 5 Заполнить таблицу

В каком порядке обход выдаст узлы дерева 1(2(4, 5), 3) — корень 1, у него дети 2 и 3, у 2 — дети 4 и 5?

1 2 4 5 34 2 5 1 34 5 2 3 11 2 3 4 5
прямой (preorder): узел, левое, правое
симметричный (inorder): левое, узел, правое
обратный (postorder): левое, правое, узел
в ширину (по уровням)
Отметьте ячейку — «+», пустая — «−».

Войдите, чтобы отмечать пройденные уроки.