MidТеория9 min

Дерево поиска BST

Binary Search Tree: поиск, вставка, удаление и вопрос балансировки

Дерево поиска BST (Binary Search Tree)

Свойство BST

Для каждого узла N: все значения в левом поддереве < N, все в правом поддереве > N.

        8
       / \
      3   10
     / \    \
    1   6   14
       / \  /
      4  7 13

Inorder: 1, 3, 4, 6, 7, 8, 10, 13, 14 (отсортировано!)

Поиск — O(h)

<?php
declare(strict_types=1);

function searchBst(?TreeNode $root, int $target): ?TreeNode
{
    if ($root === null) {
        return null;
    }
    if ($target === $root->val) {
        return $root;
    }

    return $target < $root->val
        ? searchBst($root->left, $target)
        : searchBst($root->right, $target);
}

// Iterative
function searchBstIter(?TreeNode $root, int $target): ?TreeNode
{
    while ($root !== null) {
        if ($target === $root->val) {
            return $root;
        }
        $root = $target < $root->val ? $root->left : $root->right;
    }

    return null;
}
Визуализация поиска числа 7:
        8       8 > 7, идём влево
       / \
      3   10    3 < 7, идём вправо
     / \
    1   6       6 < 7, идём вправо
       / \
      4   7     7 == 7, нашли!

Вставка — O(h)

<?php
declare(strict_types=1);

function insertBst(?TreeNode $root, int $val): TreeNode
{
    if ($root === null) {
        return new TreeNode($val);
    }

    if ($val < $root->val) {
        $root->left = insertBst($root->left, $val);
    } elseif ($val > $root->val) {
        $root->right = insertBst($root->right, $val);
    }

    return $root;
}
Вставка 5 в BST:
        8              8
       / \            / \
      3   10   ->    3   10
     / \    \       / \    \
    1   6   14     1   6   14
       / \            / \
      4   7          4   7
                    /
                   5  <- новый узел

Удаление — O(h)

Три случая:

  1. Лист — просто удаляем
  2. Один потомок — заменяем узел потомком
  3. Два потомка — заменяем inorder-преемником (наименьший в правом поддереве)
<?php
declare(strict_types=1);

function deleteBst(?TreeNode $root, int $key): ?TreeNode
{
    if ($root === null) {
        return null;
    }

    if ($key < $root->val) {
        $root->left = deleteBst($root->left, $key);
    } elseif ($key > $root->val) {
        $root->right = deleteBst($root->right, $key);
    } else {
        // Found the node to delete
        // Case 1 & 2: missing one child
        if ($root->left === null) {
            return $root->right;
        }
        if ($root->right === null) {
            return $root->left;
        }

        // Case 3: two children
        // Find inorder successor (minimum of right subtree)
        $successor = $root->right;
        while ($successor->left !== null) {
            $successor = $successor->left;
        }

        $root->val = $successor->val;
        $root->right = deleteBst($root->right, $successor->val);
    }

    return $root;
}
Удаление узла 3 (два потомка):
        8                    8
       / \                  / \
      3   10    ->         4   10
     / \    \             / \    \
    1   6   14           1   6   14
       / \                  / \
      4   7                5   7
       \
        5

Inorder-преемник 3 = 4 (минимум правого поддерева)
Заменяем значение 3 на 4, удаляем старый узел 4

Задача: Validate BST

<?php
declare(strict_types=1);

function isValidBst(?TreeNode $root): bool
{
    return validate($root, PHP_INT_MIN, PHP_INT_MAX);
}

function validate(?TreeNode $node, int $minVal, int $maxVal): bool
{
    if ($node === null) {
        return true;
    }
    if ($node->val <= $minVal || $node->val >= $maxVal) {
        return false;
    }

    return validate($node->left, $minVal, $node->val)
        && validate($node->right, $node->val, $maxVal);
}
## Задача: Lowest Common Ancestor (BST)

В BST это проще, чем в обычном дереве:

<?php
declare(strict_types=1);

function lcaBst(?TreeNode $root, TreeNode $p, TreeNode $q): ?TreeNode
{
    while ($root !== null) {
        if ($p->val < $root->val && $q->val < $root->val) {
            $root = $root->left;      // Both on the left
        } elseif ($p->val > $root->val && $q->val > $root->val) {
            $root = $root->right;     // Both on the right
        } else {
            return $root;             // Split point — this is LCA
        }
    }

    return null;
}
## Задача: Kth Smallest Element
<?php
declare(strict_types=1);

function kthSmallest(?TreeNode $root, int $k): int
{
    $stack = [];
    $current = $root;

    while ($current !== null || $stack !== []) {
        while ($current !== null) {
            $stack[] = $current;
            $current = $current->left;
        }

        $current = array_pop($stack);
        $k--;
        if ($k === 0) {
            return $current->val;
        }

        $current = $current->right;
    }

    throw new \RuntimeException('k is out of range');
}
## Проблема балансировки

BST гарантирует O(h), но h может быть O(n) для вырожденного дерева.

Сбалансированное:          Вырожденное (вставка 1,2,3,4,5):
      3                    1
     / \                    \
    2   4                    2
   /     \                    \
  1       5                    3
                                \
h = 2, O(log n)                  4
                                  \
                                   5
                            h = 4, O(n)
Операция Сбалансированное Вырожденное
Поиск O(log n) O(n)
Вставка O(log n) O(n)
Удаление O(log n) O(n)

Решения: AVL-деревья и Red-Black деревья (следующая глава).

Запомни: BST = левое < узел < правое. Inorder обход = отсортированные данные. Все операции O(h), где h = log n для сбалансированного и n для вырожденного. Удаление узла с двумя потомками: заменить inorder-преемником. Валидация BST — через передачу диапазона (min, max).

Итоги

  1. BST: left < node < right для каждого узла
  2. Поиск, вставка, удаление = O(h)
  3. Inorder обход BST = отсортированная последовательность
  4. Удаление с двумя потомками: найти inorder-преемника
  5. Без балансировки BST может деградировать до O(n)

Проверь себя

В задаче LCA (Lowest Common Ancestor) для BST с корнем 6, если p=2 и q=8, что вернёт алгоритм?

Если вставить элементы 1, 2, 3, 4, 5 последовательно в пустое BST, какова будет высота дерева?

Для нахождения k-го наименьшего элемента в BST используется итеративный inorder обход. Какова сложность по времени?

Функция `validate($node, $minVal, $maxVal)` проверяет BST. Почему недостаточно проверить `left < node < right` только для непосредственных потомков?

В BST удаляется узел с двумя потомками. Чем его заменяют?