Деревья и обходы
Термины
Дерево — узлы, связанные «родитель — дети», без циклов, с одним корнем. Двоичное дерево — у каждого узла не больше двух детей: левый и правый.
| Термин | Значение |
|---|---|
| корень | узел без родителя |
| лист | узел без детей |
| глубина узла | расстояние от корня |
| высота дерева | длина самого длинного пути от корня до листа |
| поддерево | узел со всеми потомками |
| сбалансированное | высоты поддеревьев любого узла отличаются не больше чем на 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
Высота дерева. Верните максимальную глубину двоичного дерева (число узлов на самом длинном пути
от корня до листа). Пустое дерево — 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
class TreeNode:
def __init__(self, val, left=None, right=None):
self.val, self.left, self.right = val, left, right
def build(values):
"""Дерево из списка по уровням, None — пустое место: [3, 9, 20, None, None, 15, 7]."""
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 max_depth(root):
if root is None:
return 0
return 1 + max(max_depth(root.left), max_depth(root.right))
Обход по уровням. Верните значения узлов по уровням: список списков.
[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]]
from collections import deque
class TreeNode:
def __init__(self, val, left=None, right=None):
self.val, self.left, self.right = val, left, right
def build(values):
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 level_order(root):
if root is None:
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
Корректное дерево поиска. Проверьте, является ли дерево двоичным деревом поиска (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]))
class TreeNode:
def __init__(self, val, left=None, right=None):
self.val, self.left, self.right = val, left, right
def build(values):
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 is_valid_bst(root, low=float("-inf"), high=float("inf")):
if root is None:
return True
if not low < root.val < high:
return False
return is_valid_bst(root.left, low, root.val) and is_valid_bst(root.right, root.val, high)
Наименьший общий предок в 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
class TreeNode:
def __init__(self, val, left=None, right=None):
self.val, self.left, self.right = val, left, right
def build(values):
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 lca_bst(root, p, q):
node = root
while node:
if p < node.val and q < node.val:
node = node.left
elif p > node.val and q > node.val:
node = node.right
else:
return node.val
В каком порядке обход выдаст узлы дерева 1(2(4, 5), 3) — корень 1, у него дети 2 и 3, у 2 — дети 4 и 5?
| 1 2 4 5 3 | 4 2 5 1 3 | 4 5 2 3 1 | 1 2 3 4 5 | |
|---|---|---|---|---|
| прямой (preorder): узел, левое, правое | ||||
| симметричный (inorder): левое, узел, правое | ||||
| обратный (postorder): левое, правое, узел | ||||
| в ширину (по уровням) |
Войдите, чтобы отмечать пройденные уроки.