Validate Binary Search Tree
Cây nhị phân được lưu trong mảng tree theo thứ tự heap: nút gốc nằm ở chỉ số 0, các nút con của chỉ số i nằm ở 2*i+1 (bên trái) và 2*i+2 (bên phải), -1 đánh dấu vị trí trống, và mảng có thể kết thúc bằng các phần tử -1 thừa.
Viết isValidBST, hàm này trả về true nếu cây là cây tìm kiếm nhị phân và false nếu không phải. Trong cây tìm kiếm nhị phân, giá trị của mỗi nút phải lớn hơn nghiêm ngặt mọi giá trị trong cây con bên trái và nhỏ hơn nghiêm ngặt mọi giá trị trong cây con bên phải.
Hàm
- treeinteger-array
- cây nhị phân theo thứ tự từng tầng, dùng -1 cho vị trí trống
- Trả vềboolean
- đúng nếu cây là cây tìm kiếm nhị phân, sai nếu không
Ràng buộc
1 ≤ tree.length ≤ 32767- Mỗi
tree[i]là-1hoặc một giá trị thỏa mãn0 ≤ tree[i] ≤ 105. tree[0]không bao giờ là-1, vì vậy cây có ít nhất một nút.- Mảng có thể kết thúc bằng các phần tử
-1thừa sau nút cuối cùng. - Hai nút con của vị trí trống cũng đều trống, và độ sâu tối đa là
14. - Giá trị có thể lặp lại.
Ví dụ
- Đầu vào
- tree = [8, 3, 12, 1, 6, 10, 15]
- Đầu ra
- true
- Giải thích
- Đọc theo thứ tự (cây con trái, nút, cây con phải), ta có các giá trị
1, 3, 6, 8, 10, 12, 15. Chúng tăng nghiêm ngặt, nên mỗi nút nằm đúng phía so với mọi nút tổ tiên.
- Đầu vào
- tree = [10, 5, 15, -1, -1, 6, 20]
- Đầu ra
- false
- Giải thích
- Mỗi nút đều lớn hơn nút con bên trái và nhỏ hơn nút con bên phải, nhưng
6ở chỉ số5nằm trong cây con bên phải của nút gốc, nên nó phải lớn hơn10.
- Đầu vào
- tree = [12, 7, 12]
- Đầu ra
- false
- Giải thích
- Nút con bên phải của nút gốc chứa
12, bằng với nút gốc. Cây con bên phải phải có giá trị lớn hơn hẳn, nên cây này không hợp lệ.
Câu hỏi mở rộng
Một nút có nút cha ở chỉ số (i-1)/2, làm tròn xuống. Bạn có thể duyệt cây theo thứ tự với bộ nhớ phụ O(1), di chuyển qua các nút cha thay vì dùng ngăn xếp hay đệ quy không?
Gợi ý
Mở từng gợi ý một. Mỗi gợi ý tiết lộ thêm một chút.
Trong
[10, 5, 15, -1, -1, 6, 20], mỗi nút đều lớn hơn nút con bên trái và nhỏ hơn nút con bên phải. Tại sao đây vẫn không phải là cây tìm kiếm?Mỗi nút tổ tiên đặt ra một giới hạn cho nút: giới hạn dưới nếu nút nằm bên trái, giới hạn trên nếu nút nằm bên phải. Các giới hạn này kết hợp thành một khoảng mở. Đi sang trái từ giá trị
vsẽ hạ giới hạn trên xuốngv; đi sang phải sẽ nâng giới hạn dưới lênv.Duy trì một ngăn xếp gồm
(index, low, high), bắt đầu từ nút gốc với phạm vi rộng hơn mọi giá trị được phép. Lấy một mục ra khỏi ngăn xếp, trả về thất bại nếu giá trị không nằm строго trong phạm vi, rồi thêm từng nút con thực sự cùng phạm vi đã thu hẹp vào ngăn xếp.
Lời giải
Quy tắc này áp dụng cho toàn bộ cây con, vì vậy chỉ so sánh mỗi nút với hai nút con là chưa đủ: một nút nằm sâu trong cây có thể vi phạm giới hạn do một nút tổ tiên cách đó vài cấp đặt ra. Bạn có thể duyệt cây theo thứ tự và kiểm tra xem các giá trị có tăng nghiêm ngặt hay không, hoặc truyền cho mỗi nút phạm vi mà các nút tổ tiên cho phép.
So sánh từng nút với toàn bộ cây con của nó
Ý tưởng
Mọi giá trị ở nhánh trái của một nút đều nhỏ hơn giá trị v khi và chỉ khi giá trị lớn nhất ở đó nhỏ hơn v, và mọi giá trị ở nhánh phải đều lớn hơn v khi và chỉ khi giá trị nhỏ nhất ở đó lớn hơn v. Hai hàm đệ quy trợ giúp tìm các giá trị này. Nhánh rỗng trả về -1 hoặc 100001, nằm ngoài phạm vi giá trị, nên điều kiện luôn được thỏa mãn.
Mỗi nút được duyệt một lần cho mỗi nút tổ tiên, tức khoảng n × h lượt duyệt với cây có độ sâu h. Với độ sâu tối đa là 14, cách này không tốn nhiều chi phí, nhưng với một đường đi gồm n nút, độ phức tạp tăng lên O(n²).
Thuật toán
- Duyệt qua mọi chỉ số
icó giá trị khác-1. - Tìm giá trị lớn nhất trong cây con trái tại
2*i+1(-1nếu cây con rỗng) và giá trị nhỏ nhất trong cây con phải tại2*i+2(100001nếu cây con rỗng). - Nếu giá trị lớn nhất lớn hơn hoặc bằng
tree[i], hoặc giá trị nhỏ nhất nhỏ hơn hoặc bằngtree[i], trả vềfalse. - Trả về
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 TrueCác giá trị theo thứ tự duyệt giữa phải tăng nghiêm ngặt
Ý tưởng
Phép duyệt theo thứ tự giữa sẽ thăm cây con trái, rồi đến nút, sau đó đến cây con phải, vì vậy trong cây tìm kiếm nhị phân, phép duyệt này đọc các giá trị theo thứ tự đã sắp xếp.
Điều ngược lại cũng đúng. Với mọi nút v, toàn bộ cây con trái của nó đứng ngay trước nó trong dãy, còn toàn bộ cây con phải đứng ngay sau nó. Nếu dãy tăng nghiêm ngặt, mọi giá trị đứng trước v đều nhỏ hơn và mọi giá trị đứng sau nó đều lớn hơn, nên quy tắc này đúng tại v cũng như tại mọi nút khác.
Ví dụ thứ hai cho dãy 5, 10, 6, 15, 20: mức giảm từ 10 xuống 6 làm lộ ra nút đặt sai vị trí. Mỗi nút được thăm một lần: thời gian O(n) và bộ nhớ O(n) cho danh sách.
Thuật toán
- Viết
walk(i): nếu vị trí trống thì dừng lại, nếu không, duyệt2*i+1, thêmtree[i], rồi duyệt2*i+2. - Gọi
walk(0)để thu thập các giá trị theo thứ tự. - Với mỗi vị trí
kbắt đầu từ1, nếuvalues[k-1] ≥ values[k], trả vềfalse. - Trả về
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 TrueĐưa phạm vi cho phép xuống cây
Ý tưởng
Mỗi nút tổ tiên đặt ra một giới hạn cho nút: nhỏ hơn a nếu nút nằm trong cây con bên trái của nút tổ tiên có giá trị a, lớn hơn a nếu nút nằm trong cây con bên phải. Các giới hạn kết hợp thành một khoảng mở (low, high), và nút nằm đúng vị trí khi giá trị của nó nằm nghiêm ngặt trong khoảng đó.
Tạo khoảng khi đi xuống. Nút con bên trái giữ nguyên low và nhận high = v; nút con bên phải giữ nguyên high và nhận low = v. Trong ví dụ thứ hai, 15 truyền khoảng (10, 15) cho nút con bên trái, và 6 bị loại ngay lập tức. Các giá trị nằm trong khoảng từ 0 đến 10^5, nên -1 và 100001 đóng vai trò là không có giới hạn. Mỗi nút được kiểm tra một lần, với thời gian O(n) và O(h) nút trên ngăn xếp.
Thuật toán
- Đưa
(0, -1, 100001)vào ngăn xếp: nút gốc, không có giới hạn thực sự. - Lấy
(i, low, high)ra. Nếutree[i]không nằm строго giữalowvàhigh, trả vềfalse. - Đưa từng nút con thực sự vào ngăn xếp: nút bên trái ở
2*i+1với(low, tree[i]), nút bên phải ở2*i+2với(tree[i], high). - Khi ngăn xếp trống, trả về
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
Lỗi thường gặp và trường hợp biên
- Chỉ so sánh một nút với các nút con của nó. Trong
[10, 5, 15, -1, -1, 6, 20], mọi cặp nút cha và nút con đều có vẻ hợp lệ, nhưng6vượt quá giới hạn của nút gốc. - Cho phép các giá trị bằng nhau.
[12, 7, 12]không hợp lệ: hãy so sánh bằng<, không bao giờ dùng≤. - Dùng
0làm giá trị dưới “không giới hạn”. Cách này loại bỏ cây hợp lệ[0]. - Đọc nút con mà không kiểm tra
2*i+1 < tree.length. Trong[12, 7, 12], chỉ số3nằm ngoài phạm vi. - Trong Lua và R, mảng bắt đầu từ 1. Giữ chỉ số nút bắt đầu từ 0 để dùng
2*i+1và đọctree[i + 1].
Câu hỏi thường gặp3
Độ phức tạp thời gian của việc xác thực cây tìm kiếm nhị phân là bao nhiêu?
Phép kiểm tra thứ tự trung vị và phép kiểm tra khoảng giá trị duyệt qua mỗi nút một lần: thời gian O(n), với bộ nhớ phụ O(h) cho ngăn xếp của phép kiểm tra khoảng giá trị trên cây có độ sâu h. So sánh mỗi nút với toàn bộ cây con của nó tốn O(n × h), đạt O(n²) trên cây có hình dạng như một đường đi.
Bạn có thể kiểm tra một cây BST bằng cách duyệt theo thứ tự mà không lưu mọi giá trị không?
Đúng vậy. Phép kiểm tra chỉ so sánh mỗi giá trị với giá trị đứng trước, vì vậy hãy lưu giá trị trước đó vào một biến thay vì dùng danh sách, rồi trả về false ngay khi gặp một giá trị không lớn hơn. Bộ nhớ phụ giảm xuống còn O(h) cho đệ quy hoặc ngăn xếp.
Cây tìm kiếm nhị phân có thể chứa các giá trị trùng lặp không?
Không theo định nghĩa nghiêm ngặt dùng ở đây: giá trị bằng nhau không phù hợp với bên nào. Một số giáo trình cho phép giá trị bằng nhau ở một bên, chẳng hạn bên phải. Theo quy tắc đó, một phép so sánh nghiêm ngặt trở thành ≤.
Bài tập tương tự
Các bài tập dùng cùng ý tưởng. Giải hai hoặc ba bài là bạn sẽ nhớ được mẫu này.
def isValidBST(tree):
# Viết code ở đâyKiểm thử #1
Kiểm thử #2
Kiểm thử #3
Đối số
tree = [8, 3, 12, 1, 6, 10, 15]
Đầu ra mong đợi
true