Validate Binary Search Tree
Otrzymujesz drzewo binarne zapisane w tablicy tree w porządku poziomami. Korzeń znajduje się pod indeksem 0, dzieci węzła o indeksie i znajdują się pod indeksami 2*i+1 (lewe) i 2*i+2 (prawe), -1 oznacza puste miejsce, a tablica może kończyć się dodatkowymi wpisami -1.
Napisz funkcję o nazwie isValidBST, która zwraca true, jeśli drzewo jest binarnym drzewem wyszukiwań, a w przeciwnym razie false. W binarnym drzewie wyszukiwań wartość każdego węzła jest ściśle większa od wszystkich wartości w jego lewym poddrzewie i ściśle mniejsza od wszystkich wartości w jego prawym poddrzewie. Dwie równe wartości nigdy nie mogą jednocześnie występować w poprawnym drzewie.
Funkcja
- treeinteger-array
- drzewo binarne w porządku poziomami, z wartością -1 oznaczającą puste miejsce
- Zwracaboolean
- true, jeśli drzewo jest binarnym drzewem wyszukiwania, w przeciwnym razie false
Ograniczenia
1 ≤ tree.length ≤ 32767- Każde
tree[i]ma wartość-1lub wartość spełniającą warunek0 ≤ tree[i] ≤ 105. tree[0]nigdy nie jest równe-1, więc drzewo ma co najmniej jeden węzeł.- Tablica może kończyć się dodatkowymi wpisami
-1za ostatnim węzłem. - Oboje dzieci pustego miejsca również są puste, a głębokość wynosi najwyżej
14. - Wartości mogą się powtarzać.
Przykłady
- Wejście
- tree = [8, 3, 12, 1, 6, 10, 15]
- Wyjście
- true
- Wyjaśnienie
- Każdy węzeł znajduje się po właściwej stronie każdego węzła nad nim. Odczytane w kolejności (lewe poddrzewo, węzeł, prawe poddrzewo) wartości to
1, 3, 6, 8, 10, 12, 15, czyli ciąg ściśle rosnący — właśnie taki, jaki daje drzewo wyszukiwań.
- Wejście
- tree = [10, 5, 15, -1, -1, 6, 20]
- Wyjście
- false
- Wyjaśnienie
- Każdy węzeł jest większy od swojego lewego dziecka i mniejszy od prawego, a mimo to drzewo jest nieprawidłowe. Wartość
6na indeksie5znajduje się w prawym poddrzewie korzenia10, więc musi być większa niż10, a tak nie jest.
- Wejście
- tree = [12, 7, 12]
- Wyjście
- false
- Wyjaśnienie
- Prawe dziecko korzenia ma wartość
12, taką samą jak korzeń. Prawe poddrzewo musi być ściśle większe, więc równa wartość narusza tę regułę.
+16 ukrytych testów przy wysłaniu
Pytanie dodatkowe
Rodzic węzła o indeksie i znajduje się pod indeksem (i-1)/2, zaokrąglonym w dół. Czy potrafisz przejść drzewo w kolejności inorder, używając dodatkowo O(1) miejsca i przemieszczając się przez rodziców zamiast używać stosu lub rekurencji?
Podpowiedzi
Otwieraj je po kolei. Każda zdradza trochę więcej.
W
[10, 5, 15, -1, -1, 6, 20]każdy węzeł jest większy od lewego dziecka i mniejszy od prawego. Dlaczego to nadal nie jest drzewo wyszukiwań?Każdy przodek wyznacza granicę dla węzła: poniżej swojej wartości, jeśli węzeł znajduje się po jego lewej stronie, a powyżej swojej wartości, jeśli znajduje się po prawej. Razem te granice tworzą jeden otwarty przedział. Przejście w lewo od wartości
vobniża górną granicę dov; przejście w prawo podnosi dolną granicę dov.Utrzymuj stos
(index, low, high), zaczynając od korzenia i zakresu szerszego niż każda dozwolona wartość. Zdejmij element ze stosu, zakończ niepowodzeniem, jeśli wartość nie mieści się ściśle w zakresie, i dodaj każde rzeczywiste dziecko z zawężonym zakresem.
Rozwiązanie
Ta reguła dotyczy całych poddrzew, a nie węzła i jego dwojga dzieci. Drzewo może przejść test rodzica i dziecka w każdym węźle, a mimo to być nieprawidłowe, ponieważ węzeł położony głęboko w drzewie może naruszyć ograniczenie ustalone przez przodka znajdującego się kilka poziomów wyżej. Można to łatwo rozwiązać na dwa sposoby: odczytać drzewo w kolejności i sprawdzić, czy wartości ściśle rosną, albo przekazać każdemu węzłowi zakres wartości dozwolonych przez jego przodków i sprawdzić, czy mieści się w tym zakresie.
Porównaj każdy węzeł z całymi jego poddrzewami
Intuicja
Najpierw zobaczmy, jak poruszać się po tablicy. Węzeł o indeksie i ma lewe dziecko pod indeksem 2*i+1, a prawe pod indeksem 2*i+2. Dziecko istnieje tylko wtedy, gdy jego indeks znajduje się w tablicy, a wartość pod tym indeksem nie jest równa -1. W tablicy [10, 5, 15, -1, -1, 6, 20] korzeń 10 ma węzły 5 i 15 pod indeksami 1 i 2, a węzeł 15 ma węzły 6 i 20 pod indeksami 5 i 6.
Pierwszy pomysł, na który wpada większość osób, polega na porównaniu każdego węzła tylko z jego dwojgiem dzieci. Właśnie dlatego to drzewo pokazuje, że takie podejście nie działa: 5 < 10, 15 > 10, 6 < 15 i 20 > 15 są prawdziwe, ale węzeł 6 znajduje się na prawo od 10. Definicja mówi o każdej wartości w poddrzewie, więc sprawdźmy dokładnie to.
Dla węzła przechowującego v wszystkie wartości po jego lewej stronie są mniejsze od v dokładnie wtedy, gdy największa wartość po jego lewej stronie jest mniejsza od v. Podobnie wszystkie wartości po jego prawej stronie są większe od v, gdy najmniejsza wartość po tej stronie jest większa od v. Dwie niewielkie funkcje rekurencyjne znajdują tę największą i najmniejszą wartość. Dla pustej strony największą wartością jest -1, a najmniejszą 100001 — są to wartości spoza dozwolonego zakresu, więc pusta strona nigdy nie spowoduje niepowodzenia.
To rozwiązanie jest poprawne, ale powtarza pracę. Węzeł jest przeglądany raz dla każdego swojego przodka, więc łączna liczba odwiedzin wynosi około n × h dla drzewa o głębokości h. Przy głębokości nieprzekraczającej 14 nie stanowi to tutaj problemu, ale w drzewie będącym jedną długą ścieżką złożoną z n węzłów złożoność rośnie do O(n²).
Algorytm
- Przejdź przez każdy indeks
i, którego wartość nie jest równa-1. - Znajdź największą wartość w lewym poddrzewie zaczynającym się od
2*i+1albo-1, jeśli to miejsce jest puste. - Znajdź najmniejszą wartość w prawym poddrzewie zaczynającym się od
2*i+2albo100001, jeśli to miejsce jest puste. - Jeśli największa wartość jest większa lub równa
tree[i]albo najmniejsza wartość jest mniejsza lub równatree[i], zwróćfalse. - Po ostatnim węźle zwróć
true.
def isValidBST(tree):
n = len(tree)
def largest(i):
# Largest value in the subtree at index i, or -1 when that spot is empty.
if i >= n or tree[i] == -1:
return -1
return max(tree[i], largest(2 * i + 1), largest(2 * i + 2))
def smallest(i):
# Smallest value in the subtree at index i, or 100001 when that spot is empty.
if i >= n or tree[i] == -1:
return 100001
return min(tree[i], smallest(2 * i + 1), smallest(2 * i + 2))
for i in range(n):
if tree[i] == -1:
continue
# Everything on the left must be smaller, everything on the right larger.
if largest(2 * i + 1) >= tree[i] or smallest(2 * i + 2) <= tree[i]:
return False
return TrueWartości w porządku inorder muszą ściśle rosnąć
Intuicja
Przejście inorder odwiedza lewe poddrzewo, następnie węzeł, a potem prawe poddrzewo. W drzewie przeszukiwań binarnych ta kolejność jest posortowana: wszystko po lewej stronie jest mniejsze, więc pojawia się wcześniej, a wszystko po prawej stronie jest większe, więc pojawia się później. Pierwszy przykład daje 1, 3, 6, 8, 10, 12, 15.
Zależność działa też w drugą stronę i właśnie dlatego jest to test. Weź dowolny węzeł v. W sekwencji inorder całe jego lewe poddrzewo znajduje się bezpośrednio przed nim, a całe prawe poddrzewo — bezpośrednio po nim. Jeśli sekwencja jest ściśle rosnąca, każda wartość przed v jest mniejsza, a każda wartość po nim jest większa, więc reguła jest spełniona dla v i tak samo dla każdego innego węzła.
Przejdź więc drzewo w kolejności inorder, zbierz wartości i sprawdź każdą z nich względem poprzedniej. Drugi przykład daje 5, 10, 6, 15, 20: krok od 10 w dół do 6 ujawnia węzeł po niewłaściwej stronie. Trzeci daje 7, 12, 12, a powtórzone 12 nie przechodzi ścisłego sprawdzenia. Każdy węzeł jest odwiedzany raz, co daje czas O(n), a lista zajmuje O(n) miejsca.
Algorytm
- Napisz
walk(i): jeśli miejsce jest puste, zakończ; w przeciwnym razie przejdź do2*i+1, dodajtree[i], a następnie przejdź do2*i+2. - Wywołaj
walk(0), aby zebrać wartości w odpowiedniej kolejności. - Dla każdej pozycji
kod1, jeślivalues[k-1] ≥ values[k], zwróćfalse. - Zwróć
true.
def isValidBST(tree):
n = len(tree)
values = []
def walk(i):
# Left subtree, then the node, then the right subtree.
if i >= n or tree[i] == -1:
return
walk(2 * i + 1)
values.append(tree[i])
walk(2 * i + 2)
walk(0)
# A search tree read in order gives strictly increasing values.
for k in range(1, len(values)):
if values[k - 1] >= values[k]:
return False
return TruePrzenoś dozwolony zakres w dół drzewa
Intuicja
Spójrz na tę regułę z perspektywy węzła. Każdy przodek wyznacza dla niego jedno ograniczenie. Jeśli węzeł znajduje się w lewym poddrzewie przodka zawierającego a, jego wartość musi być mniejsza od a; jeśli znajduje się w prawym poddrzewie, musi być większa od a. Wszystkie te ograniczenia razem tworzą jeden otwarty przedział (low, high), a węzeł jest na właściwym miejscu dokładnie wtedy, gdy jego wartość leży ściśle wewnątrz tego przedziału.
Możesz wyznaczać ten przedział podczas schodzenia w dół. Korzeń nie ma ograniczeń. Przechodząc od węzła zawierającego v do jego lewego dziecka, zachowujesz low i obniżasz high do v; przechodząc do prawego dziecka, zachowujesz high i podnosisz low do v. Nowe ograniczenie jest zawsze bardziej rygorystyczne niż zastępowane, ponieważ samo v przeszło sprawdzian względem poprzedniego przedziału.
W drugim przykładzie 15 otrzymuje przedział (10, no limit) i przekazuje go swojemu lewemu dziecku jako (10, 15). Wartość 6 jest mniejsza od 10, więc sprawdzian kończy się niepowodzeniem już w tym miejscu, bez sprawdzania jakiegokolwiek innego węzła. Wartości mieszczą się w zakresie od 0 do 10^5, więc -1 i 100001 pełnią funkcję „braku ograniczenia”.
Przechowuj oczekujące węzły wraz z ich przedziałami na stosie. Każdy węzeł jest sprawdzany raz, co zajmuje O(n) czasu, a stos przechowuje oczekujące węzły leżące wzdłuż jednej ścieżki, co wymaga O(h) pamięci. Pierwszy nieprawidłowy przedział kończy wyszukiwanie.
Algorytm
- Umieść na stosie
(0, -1, 100001): indeks korzenia i otwarty zakres bez rzeczywistego ograniczenia. - Zdejmij ze stosu
(i, low, high). Jeślitree[i]nie znajduje się ściśle międzylowahigh, zwróćfalse. - Jeśli lewe dziecko
2*i+1istnieje, umieść je na stosie z zakresem(low, tree[i]). - Jeśli prawe dziecko
2*i+2istnieje, umieść je na stosie z zakresem(tree[i], high). - Gdy stos będzie pusty, zwróć
true.
def isValidBST(tree):
n = len(tree)
# Each entry: a node index and the open range (low, high) its value must fall in.
# -1 and 100001 lie outside every allowed value, so they mean "no limit".
stack = [(0, -1, 100001)]
while stack:
i, low, high = stack.pop()
value = tree[i]
if not (low < value < high):
return False
left, right = 2 * i + 1, 2 * i + 2
if left < n and tree[left] != -1:
stack.append((left, low, value)) # the left side must stay below value
if right < n and tree[right] != -1:
stack.append((right, value, high)) # the right side must stay above value
return True
Pułapki i przypadki brzegowe
Większość błędnych odpowiedzi sprawdza zbyt mało albo sprawdza właściwą rzecz, ale używa niewłaściwego porównania.
- Porównywanie węzła tylko z jego dziećmi. W
[10, 5, 15, -1, -1, 6, 20]każda para rodzic–dziecko wygląda poprawnie, ale6nadal narusza ograniczenie wyznaczone przez korzeń dwa poziomy wyżej. - Dopuszczanie równych wartości. Porządek jest ścisły po obu stronach, więc
[12, 7, 12]nie jest prawidłowe. Używajlow < v < highivalues[k-1] < values[k], nigdy≤. - Przekazywanie w dół tylko wartości rodzica. Lewe dziecko potrzebuje obu ograniczeń: musi być mniejsze od swojego rodzica i większe od dolnego ograniczenia, które miał rodzic. Przekazuj cały zakres.
- Wybieranie wartości oznaczającej „brak ograniczenia”, którą może przyjąć węzeł. Wartości zaczynają się od
0, więc dolne ograniczenie równe0odrzuciłoby prawidłowy węzeł o wartości0, jak w[0]. Zacznij od wartości mniejszej niż każda dozwolona wartość. - Odczytywanie elementu poza końcem tablicy. Przed odczytaniem dziecka sprawdź
2*i+1 < tree.lengthi traktuj-1jako brak dziecka. - Pomylenie przesunięcia w Lua i R, gdzie indeksowanie tablic zaczyna się od 1. Zachowaj indeksy węzłów od 0 na potrzeby obliczeń
2*i+1i odczytujtree[i + 1].
Najczęstsze pytania4
Dlaczego sprawdzenie każdego węzła względem jego dzieci nie wystarczy, aby zweryfikować BST?
Ta zasada obejmuje całe poddrzewa. Węzeł położony głęboko w prawym poddrzewie korzenia musi być większy od korzenia, nawet jeśli jest lewym dzieckiem znacznie większego węzła. W [10, 5, 15, -1, -1, 6, 20] wartość 6 jest poprawnym lewym dzieckiem 15, ale znajduje się po prawej stronie 10, więc to drzewo nie jest drzewem wyszukiwań. Potrzebujesz ograniczeń wynikających ze wszystkich przodków, a nie tylko z rodzica.
Jaka jest złożoność czasowa sprawdzania poprawności drzewa wyszukiwań binarnych?
Obie standardowe metody, sprawdzanie porządku środkowego i sprawdzanie zakresu, odwiedzają każdy węzeł raz, więc działają w czasie O(n). Sprawdzanie zakresu wymaga dodatkowej przestrzeni O(h) na stos, gdzie h oznacza głębokość. Porównywanie każdego węzła z całymi jego poddrzewami również działa, ale kosztuje O(n × h), co w przypadku drzewa ukształtowanego jak ścieżka daje O(n²).
Czy możesz zweryfikować BST za pomocą przejścia w kolejności bez zapisywania każdej wartości?
Tak. Sprawdzenie w kolejności inorder porównuje każdą wartość tylko z bezpośrednio poprzednią, więc przechowuj poprzednią wartość w zmiennej zamiast na liście. Przejdź drzewo w kolejności inorder rekurencyjnie lub za pomocą jawnego stosu i zwróć false, gdy tylko wartość nie będzie większa od poprzedniej. Dzięki temu dodatkowe zużycie pamięci spada do O(h).
Czy binarne drzewo wyszukiwania może zawierać zduplikowane wartości?
Nie według ścisłej definicji używanej tutaj: każda wartość po lewej stronie musi być mniejsza, a każda po prawej większa, więc dwie równe wartości nigdy nie mogą jednocześnie pasować. Niektóre podręczniki dopuszczają duplikaty po jednej stronie, na przykład równe wartości po prawej. Zgodnie z tą zasadą należałoby zmienić jedno ścisłe porównanie na ≤, dlatego przed napisaniem sprawdzenia przeczytaj definicję.
Podobne zadania
Zadania oparte na tych samych pomysłach. Rozwiązanie dwóch lub trzech utrwala schemat.
Python
def isValidBST(tree):
# Wpisz kod tutajPrzypadek 1
Przypadek 2
Przypadek 3
Wejście
tree = [8, 3, 12, 1, 6, 10, 15]
Oczekiwane
true