← Назад к вопросам

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

2.0 Middle🔥 171 комментариев
#Коллекции и структуры данных#Язык Swift

Условие

На входе подаётся Бинарное Дерево. Нужно определить, является ли это дерево Бинарным Деревом Поиска (BST).

Определение BST:

  • Для каждого узла все значения в левом поддереве меньше значения узла
  • Для каждого узла все значения в правом поддереве больше значения узла
  • Левое и правое поддеревья также являются BST

Пример:

    5
   / \
  3   7
 / \ / \
1  4 6  8

Это BST → return true

    5
   / \
  3   7
 / \
1   6

Это НЕ BST (6 > 5, но находится в левом поддереве) → return false

Требования:

  • Напишите наиболее оптимизированный по памяти алгоритм
  • Сложность по времени: O(n), где n — количество узлов

Эта задача была на двухчасовом собеседовании в лондонский офис Facebook.

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI23 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Решение

Подход

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

Структура узла дерева

public class TreeNode {
    public var val: Int
    public var left: TreeNode?
    public var right: TreeNode?
    
    public init(_ val: Int) {
        self.val = val
        self.left = nil
        self.right = nil
    }
}

Оптимальное решение (O(1) память, O(n) время)

func isValidBST(_ root: TreeNode?) -> Bool {
    return isValidBSTHelper(root, min: Int.min, max: Int.max)
}

func isValidBSTHelper(_ node: TreeNode?, min: Int, max: Int) -> Bool {
    // Пустой узел — это валидный BST
    guard let node = node else { return true }
    
    // Проверяем, что значение узла в допустимом диапазоне
    if node.val <= min || node.val >= max {
        return false
    }
    
    // Проверяем левое поддерево (max становится текущим значением)
    // Проверяем правое поддерево (min становится текущим значением)
    return isValidBSTHelper(node.left, min: min, max: node.val) &&
           isValidBSTHelper(node.right, min: node.val, max: max)
}

Почему это работает:

  1. Для левого поддерева все значения должны быть < node.val, поэтому max = node.val
  2. Для правого поддерева все значения должны быть > node.val, поэтому min = node.val
  3. Диапазон [min, max) сужается при спуске по дереву

Альтернативное решение (In-order обход)

В BST in-order обход дает отсортированный массив значений:

func isValidBSTInOrder(_ root: TreeNode?) -> Bool {
    var prev: Int? = nil
    var isValid = true
    
    func inorder(_ node: TreeNode?) {
        guard let node = node else { return }
        
        inorder(node.left)
        
        // Проверяем, что текущий элемент больше предыдущего
        if let prev = prev, node.val <= prev {
            isValid = false
            return
        }
        prev = node.val
        
        inorder(node.right)
    }
    
    inorder(root)
    return isValid
}

Решение с использованием очереди (BFS, O(n) память)

Если нужно избежать рекурсии:

func isValidBSTBFS(_ root: TreeNode?) -> Bool {
    guard let root = root else { return true }
    
    var queue: [(node: TreeNode, min: Int, max: Int)] = []
    queue.append((root, Int.min, Int.max))
    
    while !queue.isEmpty {
        let (node, min, max) = queue.removeFirst()
        
        if node.val <= min || node.val >= max {
            return false
        }
        
        if let left = node.left {
            queue.append((left, min, node.val))
        }
        
        if let right = node.right {
            queue.append((right, node.val, max))
        }
    }
    
    return true
}

Тестирование

// Тест 1: Валидный BST
let tree1 = TreeNode(5)
tree1.left = TreeNode(3)
tree1.right = TreeNode(7)
tree1.left?.left = TreeNode(1)
tree1.left?.right = TreeNode(4)
tree1.right?.left = TreeNode(6)
tree1.right?.right = TreeNode(8)

print(isValidBST(tree1))  // true

// Тест 2: Невалидный BST (6 в левом поддереве узла 5)
let tree2 = TreeNode(5)
tree2.left = TreeNode(3)
tree2.right = TreeNode(7)
tree2.left?.left = TreeNode(1)
tree2.left?.right = TreeNode(6)  // Ошибка!

print(isValidBST(tree2))  // false

// Тест 3: Граничный случай
let tree3 = TreeNode(Int.min)
tree3.right = TreeNode(Int.max)
print(isValidBST(tree3))  // true (если нет переполнения)

// Тест 4: Один узел
let tree4 = TreeNode(42)
print(isValidBST(tree4))  // true

Анализ решений

ПодходВремяПамять (стек)Память (куча)Преимущества
Min-Max диапазонO(n)O(h)*O(1)Простой, интуитивный
In-order обходO(n)O(h)*O(1)Использует свойство BST
BFS очередьO(n)O(1)O(w)**Итеративный, без рекурсии

*h — высота дерева (для сбалансированного дерева h = O(log n), в худшем случае h = O(n)) **w — максимальная ширина дерева

Важные моменты

  1. Int.min / Int.max: Если используются граничные значения, нужно быть осторожным. Можно использовать Optional<Int> вместо Int

  2. Рекурсия vs Итерация: Рекурсия проще для дерева, но может привести к stack overflow для очень глубоких деревьев

  3. Ошибка в наивном подходе:

// ❌ НЕПРАВИЛЬНО!
func isValidBSTWrong(_ node: TreeNode?) -> Bool {
    guard let node = node else { return true }
    
    // Проверяем только прямых детей!
    if let left = node.left, left.val >= node.val { return false }
    if let right = node.right, right.val <= node.val { return false }
    
    return isValidBSTWrong(node.left) && isValidBSTWrong(node.right)
}
// Это не работает! Например, левый потомок узла 5 может быть 3 < 5, 
// но его правое поддерево может содержать 6 > 5

Рекомендация для интервью

Используйте первое решение (min-max диапазон):

  • Легче объяснить интервьюеру
  • Показывает понимание BST свойств
  • Не требует дополнительной памяти (кроме стека рекурсии)
  • Если интервьюер просит оптимизацию, переходите на BFS

Использование Int.min/Int.max может вызвать уточняющие вопросы. Лучше сказать: "Можно использовать Optional<Int> для более надёжного решения".