Проверка BST и свойств симметричного обхода
Проверяйте, является ли двоичное дерево BST, передавая по дереву минимальные и максимальные границы и проверяя, что симметричный обход выдаёт отсортированную последовательность
«Проверка BST и свойств симметричного обхода» — бесплатный урок Coding Interview Prep на CoddyKit. Это урок 3 из 4. Ты можешь прочитать весь урок бесплатно ниже — а потом практиковать его прямо в браузере с встроенным редактором кода и ИИ-репетитором 24/7. Это часть пути обучения Coding Interview Prep, и твой прогресс синхронизируется между веб-версией и приложением CoddyKit. Курс Coding Interview Prep содержит 4 уроков всего.
Задача проверки BST
Проверка BST (LeetCode #98) — классическая задача на собеседованиях, которая ставит в тупик многих кандидатов. Наивный подход проверяет только, что значение каждого узла больше значения его левого потомка и меньше значения правого, но эта локальная проверка недостаточна. Узел поддерева может удовлетворять локальному правилу, но нарушать глобальное свойство BST. Правильное решение передаёт по дереву допустимые нижние и верхние границы.
class TreeNode:
def __init__(self, val=0, left=None, right=None):
self.val = val
self.left = left
self.right = right
# Why local check fails:
# 5
# / \
# 1 4
# / \
# 3 6
# Node 4's children (3, 6) satisfy local rule,
# but 4 < 5 and is in the RIGHT subtree -- BST violated!
print('Local check is insufficient -- use min/max bounds')Подход с нижней и верхней границами
Передавайте нижнюю и верхнюю границы при рекурсивном обходе. В каждом узле проверяйте, что low < node.val < high. При переходе влево обновляйте верхнюю границу значением node.val (значения левого поддерева должны быть меньше). При переходе вправо обновляйте нижнюю границу значением node.val (значения правого поддерева должны быть больше). Начните с low = -infinity и high = +infinity.
def is_valid_bst(root, low=float('-inf'), high=float('inf')):
if not root:
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))
# Valid BST:
valid = TreeNode(5)
valid.left = TreeNode(3)
valid.right = TreeNode(7)
print(is_valid_bst(valid)) # True
# Invalid BST (3 is in wrong subtree conceptually):
invalid = TreeNode(5)
invalid.left = TreeNode(1)
invalid.right = TreeNode(4)
invalid.right.left = TreeNode(3)
invalid.right.right = TreeNode(6)
print(is_valid_bst(invalid)) # False (4 < 5 in right subtree)Проверка с помощью симметричного обхода
Альтернативный подход к проверке использует свойство отсортированности BST при симметричном обходе: соберите последовательность симметричного обхода и проверьте, что она строго возрастает. Этот подход элегантен и прост для понимания. Однако для хранения последовательности он использует O(n) дополнительной памяти. Оптимизированный вариант использует один указатель prev во время обхода, чтобы проверять каждую пару, не сохраняя всю последовательность.
def is_valid_bst_inorder(root):
prev = [float('-inf')]
def inorder(node):
if not node:
return True
if not inorder(node.left):
return False
if node.val <= prev[0]: # not strictly increasing
return False
prev[0] = node.val
return inorder(node.right)
return inorder(root)
valid = TreeNode(5)
valid.left = TreeNode(3)
valid.right = TreeNode(7)
valid.left.left = TreeNode(1)
valid.left.right = TreeNode(4)
print(is_valid_bst_inorder(valid)) # True
invalid = TreeNode(5)
invalid.left = TreeNode(6) # 6 > 5 in left subtree!
print(is_valid_bst_inorder(invalid)) # FalseСравнение обоих подходов к проверке
Подход с нижней и верхней границами работает за O(n) времени и использует O(h) памяти (только границы в стеке вызовов). Подход с указателем на предыдущий элемент при симметричном обходе также работает за O(n) времени и использует O(h) памяти. Оба подхода оптимальны. Подход с нижней и верхней границами более универсален и хорошо подходит для задач с дополнительными ограничениями. На собеседованиях будьте готовы представить оба подхода и обсудить компромиссы — знание альтернатив является весомым преимуществом.
# Both approaches:
# Time: O(n) -- visit each node once
# Space: O(h) -- call stack depth
# h = O(log n) balanced, O(n) skewed
# When to choose which:
# min/max bounds:
# - Cleaner for trees with constraints beyond BST
# - No global state (purely functional)
# in-order prev:
# - More intuitive (sorted sequence check)
# - Easier to convert to iterative with a stack
print('Both O(n) time, O(h) space -- choose by clarity')Восстановление BST: два переставленных узла
Восстановление BST (LeetCode #99) исправляет BST, в котором были переставлены ровно два узла. При симметричном обходе корректно упорядоченный BST выдаёт отсортированную последовательность. Если два узла переставлены, появится одно или два нарушения, при которых prev.val > current.val. Первый узел первого нарушения и второй узел последнего нарушения — это два узла, оказавшиеся не на своих местах; переставьте их значения.
def recover_tree(root):
first = second = prev = None
def inorder(node):
nonlocal first, second, prev
if not node:
return
inorder(node.left)
if prev and prev.val > node.val:
if not first:
first = prev # first violator
second = node # always update second
prev = node
inorder(node.right)
inorder(root)
# Swap values of the two misplaced nodes
if first and second:
first.val, second.val = second.val, first.val
root = TreeNode(3)
root.left = TreeNode(1)
root.right = TreeNode(4)
root.right.left = TreeNode(2) # 2 and 3 are swapped
recover_tree(root)
print(root.val, root.right.left.val) # 2, 3 (fixed)Преобразование BST в отсортированный массив с помощью симметричного обхода
Преобразовать BST в отсортированный массив очень просто: выполните симметричный обход и соберите значения. Эта операция за O(n) времени и O(n) памяти позволяет быстро применять алгоритмы для отсортированных массивов (двоичный поиск, два указателя) к данным BST. Часто это промежуточный шаг в составных задачах о BST, например «объединить два BST» или «найти медиану BST».
def bst_to_sorted_array(root):
result = []
def inorder(node):
if not node:
return
inorder(node.left)
result.append(node.val)
inorder(node.right)
inorder(root)
return result
root = TreeNode(4)
root.left = TreeNode(2)
root.right = TreeNode(6)
root.left.left = TreeNode(1)
root.left.right = TreeNode(3)
root.right.left = TreeNode(5)
root.right.right = TreeNode(7)
print(bst_to_sorted_array(root)) # [1, 2, 3, 4, 5, 6, 7]Объединение двух BST
Чтобы объединить два BST в один отсортированный массив, преобразуйте каждый из них в отсортированный массив за O(n) и O(m), а затем объедините два отсортированных массива с помощью шага слияния сортировки слиянием за O(n+m). Общая временная сложность: O(n+m). Если результатом должен быть сбалансированный BST, передайте объединённый отсортированный массив алгоритму преобразования отсортированного массива в BST. Такое разбиение на простые подзадачи — признак ясного решения, понятного интервьюеру.
def merge_two_bsts(root1, root2):
def inorder(node, arr):
if not node:
return
inorder(node.left, arr)
arr.append(node.val)
inorder(node.right, arr)
arr1, arr2 = [], []
inorder(root1, arr1)
inorder(root2, arr2)
# Merge two sorted arrays
merged = []
i = j = 0
while i < len(arr1) and j < len(arr2):
if arr1[i] <= arr2[j]:
merged.append(arr1[i]); i += 1
else:
merged.append(arr2[j]); j += 1
merged.extend(arr1[i:])
merged.extend(arr2[j:])
return merged
r1 = TreeNode(2); r1.left = TreeNode(1); r1.right = TreeNode(4)
r2 = TreeNode(3); r2.left = TreeNode(0); r2.right = TreeNode(5)
print(merge_two_bsts(r1, r2)) # [0, 1, 2, 3, 4, 5]Подсчёт узлов BST в диапазоне
Подсчитайте количество узлов со значениями из диапазона [low, high]. Полный перебор с помощью симметричного обхода занимает O(n). Вариант с учётом свойств BST отсекает поддеревья: если значение текущего узла меньше low, проверять левое поддерево нет смысла (все его значения также меньше low). Аналогично отсекайте правое поддерево, когда текущее значение больше high. Средняя сложность составляет O(log n + k), где k — количество подходящих узлов.
def range_sum_bst(root, low, high):
if not root:
return 0
total = 0
if low <= root.val <= high:
total += root.val
if root.val > low: # left subtree may have values >= low
total += range_sum_bst(root.left, low, high)
if root.val < high: # right subtree may have values <= high
total += range_sum_bst(root.right, low, high)
return total
root = TreeNode(10)
root.left = TreeNode(5)
root.right = TreeNode(15)
root.left.left = TreeNode(3)
root.left.right = TreeNode(7)
root.right.right = TreeNode(18)
print(range_sum_bst(root, 7, 15)) # 7 + 10 + 15 = 32Повторяющиеся значения и строгий и нестрогий BST
Стандартный инвариант BST использует строгое неравенство: значения левого поддерева строго меньше, а значения правого поддерева строго больше. В некоторых задачах разрешены повторяющиеся значения: их помещают в левое поддерево (left <= root) или в правое поддерево (root < right). При проверке BST всегда обращайтесь к определению из условия задачи. Подход с нижней и верхней границами поддерживает оба варианта: нужно лишь изменить проверку границ, сделав её строгой или включающей границу.
# Strict BST (LeetCode default): left < root < right
def is_valid_strict(root, lo=float('-inf'), hi=float('inf')):
if not root:
return True
if not (lo < root.val < hi): # STRICT inequalities
return False
return (is_valid_strict(root.left, lo, root.val) and
is_valid_strict(root.right, root.val, hi))
# Non-strict BST (allows duplicates in right): left <= root < right
def is_valid_nonstrict(root, lo=float('-inf'), hi=float('inf')):
if not root:
return True
if not (lo <= root.val < hi): # NOTE: <= for left side
return False
return (is_valid_nonstrict(root.left, lo, root.val + 1) and
is_valid_nonstrict(root.right, root.val, hi))
print('Always clarify strict vs non-strict with interviewer')Симметричный обход как универсальный инструмент для BST
Симметричный обход — универсальный инструмент для задач о BST. Если в задаче о BST требуется работать с отсортированным порядком, k-м элементом, запросами по диапазону или свойствами последовательности, подумайте, даст ли ответ симметричный обход (или обратный симметричный обход). Большинство задач, специфичных для BST, сводятся к следующему: обойти дерево в отсортированном порядке и выполнить действие на каждом шаге. Умение быстро распознавать эту связь — важный навык на собеседовании.
# Problems solved elegantly with in-order:
# 1. Validate BST: check prev <= curr during in-order
# 2. Kth smallest: count k steps in in-order
# 3. Kth largest: count k steps in REVERSE in-order
# 4. Closest value to target: find crossover in in-order
# 5. BST to sorted array: collect in-order into list
# 6. Recover BST: find 1-2 violations in in-order
# 7. Sum of range [lo, hi]: accumulate during in-order
# The key insight: in-order visits BST nodes in sorted order.
# All sorted-order reasoning translates to in-order DFS.
print('In-order = sorted access = foundation of BST reasoning')Ближайшее значение в BST
Найдите узел со значением, наиболее близким к заданному целевому значению. Используйте упорядоченность BST: начните с корня, сохраняйте ближайшее из встреченных значений и двигайтесь в направлении цели (идите влево, если цель меньше, и вправо, если больше). Этот подход за O(h) эффективнее симметричного обхода и показывает, как с пользой применять свойство BST для сокращения пространства поиска.
def closest_value(root, target):
closest = root.val
curr = root
while curr:
if abs(curr.val - target) < abs(closest - target):
closest = curr.val
if target < curr.val:
curr = curr.left
elif target > curr.val:
curr = curr.right
else:
break # exact match
return closest
root = TreeNode(4)
root.left = TreeNode(2)
root.right = TreeNode(5)
root.left.left = TreeNode(1)
root.left.right = TreeNode(3)
print(closest_value(root, 3.714286)) # 4Быстрая проверка
Проверьте понимание понятий «Структуры данных и алгоритмы — подготовка к собеседованию по программированию» из этого урока.
Итоги урока
В этом уроке Вы узнали: как проверять BST с помощью нижних и верхних границ (избегая ошибки локальной проверки), альтернативный подход с указателем на предыдущий элемент при симметричном обходе и о симметричном обходе как универсальном инструменте BST для подсчёта сумм в диапазоне, поиска ближайшего значения и операций объединения. Далее мы используем свойства симметричного обхода BST для поиска k-го наименьшего элемента.
Часто задаваемые вопросы
Урок «Проверка BST и свойств симметричного обхода» бесплатный?
Да — полный текст урока «Проверка BST и свойств симметричного обхода» бесплатно доступен здесь в веб-версии. Чтобы практиковать его интерактивно (встроенный редактор кода и ИИ-репетитор 24/7) и разблокировать остальной курс Coding Interview Prep, подпишись на CoddyKit PRO. Курс Coding Interview Prep содержит 4 уроков всего.
Чему я научусь в уроке «Проверка BST и свойств симметричного обхода»?
Проверяйте, является ли двоичное дерево BST, передавая по дереву минимальные и максимальные границы и проверяя, что симметричный обход выдаёт отсортированную последовательность Ты практикуешь Coding Interview Prep с помощью реального кода, который запускаешь прямо в браузере, и ИИ-репетитор 24/7 отвечает на твои вопросы во время урока.
Нужен ли мне опыт, чтобы начать Coding Interview Prep?
Предыдущий опыт не требуется. Coding Interview Prep на CoddyKit структурирован для всех уровней — от новичков до продвинутых, поэтому ты можешь начать отсюда или с самого начала и учиться в своем темпе. Это урок 3 из 4.
Сколько времени занимает урок «Проверка BST и свойств симметричного обхода»?
Большинство уроков CoddyKit занимают около 5–10 минут. Каждый из них компактный и интерактивный, поэтому ты постоянно делаешь прогресс и продолжаешь с того же места в веб-версии и приложении.
Можно ли писать и запускать код в этом уроке Coding Interview Prep?
Да. Каждый урок Coding Interview Prep включает встроенный редактор кода, поэтому ты пишешь и запускаешь реальный код прямо в браузере и получаешь моментальную обратную связь от AI — локальная установка не требуется.
Все уроки этого курса
- Вставка и поиск в BST
- Удаление из BST: три случая
- Проверка BST и свойств симметричного обхода
- k-й наименьший элемент, сумма диапазона и преобразование BST в отсортированный массив