0Pricing
Coding Interview Prep · Lekcja

Sprawdzanie BST i właściwości inorder

Zweryfikują Państwo, czy drzewo binarne jest BST, przekazując w dół drzewa ograniczenia min/max oraz sprawdzając, czy przejście inorder tworzy posortowaną sekwencję.

Sprawdzanie BST i właściwości inorder to bezpłatna lekcja Coding Interview Prep na CoddyKit. To lekcja 3 z 4. Możesz przeczytać całą lekcję poniżej za darmo — a potem ćwiczyć ją interaktywnie w przeglądarce z wbudowanym edytorem kodu i tutorem AI dostępnym 24/7. To część ścieżki edukacyjnej Coding Interview Prep, a Twój postęp synchronizuje się między webem a aplikacją CoddyKit. Kurs Coding Interview Prep zawiera 4 lekcji w sumie.

Problem walidacji BST

Validate BST (LeetCode #98) to klasyczne zadanie rekrutacyjne, które sprawia trudność wielu kandydatom. Naiwne podejście sprawdza tylko, czy wartość każdego węzła jest większa od jego lewego dziecka i mniejsza od prawego, ale takie lokalne sprawdzenie jest niewystarczające. Węzeł w poddrzewie może spełniać lokalną regułę, a jednocześnie naruszać globalną własność BST. Poprawne rozwiązanie przekazuje w dół drzewa prawidłowe dolne i górne granice.

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')

Podejście z dolną i górną granicą

Należy przekazywać dolną i górną granicę w dół rekurencji. Dla każdego węzła trzeba sprawdzić, czy low < node.val < high. Przy przejściu do lewego poddrzewa należy ustawić górną granicę na node.val (wartości w lewym poddrzewie muszą być mniejsze). Przy przejściu do prawego poddrzewa należy ustawić dolną granicę na node.val (wartości w prawym poddrzewie muszą być większe). Należy rozpocząć od low = -infinity i 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)

Walidacja za pomocą przejścia inorder

Alternatywne podejście do walidacji wykorzystuje właściwość BST polegającą na uporządkowaniu w przejściu inorder: należy zebrać sekwencję inorder i sprawdzić, czy jest ściśle rosnąca. Jest to eleganckie i łatwe do przeanalizowania rozwiązanie. Wymaga jednak O(n) dodatkowej przestrzeni na przechowywanie sekwencji. Zoptymalizowana wersja korzysta podczas przejścia z jednego wskaźnika prev, aby sprawdzać każdą parę bez zapisywania całej sekwencji.

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

Porównanie obu podejść do walidacji

Podejście z dolną i górną granicą ma złożoność czasową O(n) i pamięciową O(h) (na stosie wywołań przechowywane są tylko granice). Podejście z wskaźnikiem prev w przejściu inorder również ma złożoność czasową O(n) i pamięciową O(h). Oba podejścia są optymalne. Podejście z granicami jest bardziej uniwersalne i dobrze sprawdza się po rozszerzeniu o dodatkowe ograniczenia. Na rozmowie kwalifikacyjnej warto być przygotowanym na przedstawienie obu podejść i omówienie kompromisów — znajomość alternatyw jest wyraźnym atutem.

# 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')

Naprawa BST: dwa zamienione węzły

Recover BST (LeetCode #99) naprawia BST, w którym dokładnie dwa węzły zamieniły się miejscami. Podczas przejścia inorder poprawnie uporządkowane BST tworzy posortowaną sekwencję. Jeśli dwa węzły zostaną zamienione, pojawi się jedno lub dwa naruszenia, w których prev.val > current.val. Pierwszy węzeł pierwszego naruszenia i drugi węzeł ostatniego naruszenia to dwa nieprawidłowo umieszczone węzły — należy zamienić ich wartości.

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 do posortowanej tablicy w kolejności inorder

Konwersja BST do posortowanej tablicy jest prosta: należy wykonać przejście inorder i zebrać wartości. Ta operacja o złożoności czasowej O(n) i pamięciowej O(n) umożliwia szybkie wykorzystanie algorytmów dla posortowanych tablic (wyszukiwania binarnego, dwóch wskaźników) na danych z BST. Często stanowi etap pośredni w wieloczęściowych zadaniach dotyczących BST, takich jak „scalanie dwóch BST” lub „znalezienie mediany 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]

Scalanie dwóch BST

Aby scalić dwa BST w jedną posortowaną tablicę, należy przekonwertować każde z nich na posortowaną tablicę w czasie O(n) i O(m), a następnie scalić te dwie tablice za pomocą etapu scalania algorytmu sortowania przez scalanie w czasie O(n+m). Łączna złożoność czasowa wynosi O(n+m). Jeśli wynik ma być zrównoważonym BST, należy przekazać scaloną posortowaną tablicę do algorytmu konwersji posortowanej tablicy na BST. Taki podział na proste podproblemy jest oznaką przejrzystego rozwiązania, które dobrze sprawdza się na rozmowie kwalifikacyjnej.

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]

Zliczanie węzłów BST w zakresie

Należy policzyć, ile węzłów ma wartości należące do zakresu [low, high]. Naiwne przejście inorder po całym drzewie ma złożoność O(n). Wersja wykorzystująca właściwości BST stosuje przycinanie: jeśli wartość bieżącego węzła jest mniejsza niż low, nie ma potrzeby sprawdzać lewego poddrzewa (wszystkie znajdujące się w nim wartości również są mniejsze niż low). Analogicznie należy pominąć prawe poddrzewo, gdy bieżąca wartość jest większa niż high. Średnia złożoność wynosi O(log n + k), gdzie k oznacza liczbę pasujących węzłów.

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

Duplikaty wartości oraz ścisłe i nieścisłe BST

Standardowy niezmiennik BST wykorzystuje nierówności ścisłe: wartości w lewym poddrzewie są ściśle mniejsze, a wartości w prawym poddrzewie — ściśle większe. Niektóre zadania dopuszczają duplikaty, umieszczając je w lewym poddrzewie (left <= root) lub w prawym poddrzewie (root < right). Podczas walidacji BST zawsze należy sprawdzić definicję przyjętą w treści zadania. Podejście z dolną i górną granicą obsługuje oba warianty, zmieniając to, czy sprawdzanie granic ma być ścisłe, czy nieścisłe.

# 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')

Przejście inorder jako uniwersalne narzędzie BST

Przejście inorder jest uniwersalnym narzędziem w zadaniach dotyczących BST. Gdy zadanie dotyczące BST odnosi się do kolejności posortowanej, k-tego elementu, zapytań o zakres lub właściwości sekwencji, warto rozważyć, czy odpowiedzi nie dostarczy przejście inorder (lub jego odwrotność). Większość problemów specyficznych dla BST sprowadza się do następującego schematu: przejść w kolejności posortowanej i wykonać operację na każdym kroku. Szybkie rozpoznawanie tego schematu jest ważną umiejętnością podczas rozmów kwalifikacyjnych.

# 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')

Najbliższa wartość w BST

Należy znaleźć węzeł, którego wartość jest najbliższa podanej wartości docelowej. Warto wykorzystać uporządkowanie BST: rozpocząć od korzenia, śledzić dotychczas znalezioną najbliższą wartość i poruszać się w kierunku wartości docelowej (przejść w lewo, jeśli wartość docelowa jest mniejsza, lub w prawo, jeśli jest większa). To podejście o złożoności O(h) jest wydajniejsze niż przejście inorder i pokazuje skuteczne wykorzystanie własności BST do ograniczania przestrzeni wyszukiwania.

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

Sprawdzenie wiedzy

Proszę sprawdzić, czy rozumieją Państwo pojęcia z lekcji Data Structures & Algorithms — Coding Interview Prep.

Podsumowanie lekcji

W tej lekcji poznali Państwo: walidację BST z użyciem dolnej i górnej granicy (zapobiegającą problemowi lokalnego sprawdzania), alternatywę z wskaźnikiem prev w przejściu inorder oraz przejście inorder jako uniwersalne narzędzie do obliczania sum zakresów, znajdowania najbliższej wartości i scalania. W dalszej części wykorzystamy właściwości przejścia inorder BST do znalezienia k-tego najmniejszego elementu.

Często zadawane pytania

Czy lekcja „Sprawdzanie BST i właściwości inorder” jest bezpłatna?

Tak — pełny tekst „Sprawdzanie BST i właściwości inorder” jest dostępny za darmo tutaj w sieci. Aby ćwiczyć ją interaktywnie (wbudowany edytor kodu i tutor AI dostępny 24/7) i odblokować resztę kursu Coding Interview Prep, przejdź na CoddyKit PRO. Kurs Coding Interview Prep zawiera 4 lekcji w sumie.

Co nauczysz się w „Sprawdzanie BST i właściwości inorder”?

Zweryfikują Państwo, czy drzewo binarne jest BST, przekazując w dół drzewa ograniczenia min/max oraz sprawdzając, czy przejście inorder tworzy posortowaną sekwencję. Ćwiczysz Coding Interview Prep z praktycznym kodem, który uruchamiasz bezpośrednio w przeglądarce, a tutor AI dostępny 24/7 odpowiada na Twoje pytania podczas pracy nad lekcją.

Czy potrzebuję doświadczenia, aby zacząć Coding Interview Prep?

Nie wymagamy żadnego doświadczenia. Coding Interview Prep w CoddyKit jest strukturyzowany dla początkujących i zaawansowanych użytkowników, więc możesz zacząć tutaj lub od początku i uczyć się w swoim tempie. To lekcja 3 z 4.

Ile czasu zajmuje lekcja „Sprawdzanie BST i właściwości inorder”?

Większość lekcji CoddyKit trwa około 5–10 minut. Każda lekcja to mały, interaktywny krok, dzięki czemu robisz systematyczne postępy i zawsze wracasz dokładnie do tego samego miejsca — na webie i w aplikacji.

Czy mogę pisać i uruchamiać kod w tej lekcji Coding Interview Prep?

Tak. Każda lekcja Coding Interview Prep zawiera wbudowany edytor kodu, więc piszesz i uruchamiasz prawdziwy kod bezpośrednio w przeglądarce i od razu otrzymujesz sprzężenie zwrotne od AI — bez konfiguracji na komputerze.

Wszystkie lekcje w tym kursie

  1. Wstawianie i wyszukiwanie w BST
  2. Usuwanie z BST: trzy przypadki
  3. Sprawdzanie BST i właściwości inorder
  4. K-ty najmniejszy element, suma zakresu i BST do posortowanej tablicy
← Powrót do Coding Interview Prep