Lập trình · Cấu trúc dữ liệu
Duyệt cây nhị phân: preorder, inorder, postorder và level order
Trả lời nhanh
Duyệt cây nhị phân nghĩa là thăm mỗi node đúng một lần theo một thứ tự cố định. Ba thứ tự theo chiều sâu chỉ khác nhau ở thời điểm thăm chính node: preorder thăm node, trái, phải; inorder thăm trái, node, phải; postorder thăm trái, phải, node. Duyệt level-order theo chiều rộng và thăm cây theo từng hàng bằng hàng đợi. Cả bốn đều tốn O(n).
Tại sao thứ tự duyệt quan trọng
Cây không có một thứ tự tự nhiên duy nhất, nên thuật toán chọn thứ tự phù hợp với bài toán. Duyệt inorder một cây tìm kiếm nhị phân cho ra các khóa theo thứ tự tăng dần. Preorder thăm cha trước con, đúng thứ bạn cần để sao chép cây hoặc ghi cây ra để dựng lại được. Postorder thăm con trước cha, đúng thứ bạn cần để xóa cây, tính kích thước thư mục hoặc tính giá trị cây biểu thức. Level order tìm node nông nhất trước và in cây theo từng hàng.
Bốn thứ tự trên cùng một ví dụ
Ta dùng cây này: gốc 1 có hai con là 2 và 3; node 2 có hai con là 4 và 5; node 3 có con phải là 6.
1
/ \
2 3
/ \ \
4 5 6| Thứ tự | Quy tắc | Kết quả |
|---|---|---|
| Preorder | node, trái, phải | 1 2 4 5 3 6 |
| Inorder | trái, node, phải | 4 2 5 1 3 6 |
| Postorder | trái, phải, node | 4 5 2 6 3 1 |
| Level order | từng hàng, từ trái sang phải | 1 2 3 4 5 6 |
Cách nhớ ba thứ tự đầu đơn giản: tên cho biết node nằm ở đâu. Pre là trước cả hai cây con, in là giữa hai cây con, post là sau cả hai. Cây con trái luôn được thăm trước cây con phải.
Cài đặt đệ quy
Các phiên bản đệ quy phản ánh trực tiếp định nghĩa. Mỗi hàm thăm node ở một dòng khác nhau.
class Node:
def __init__(self, val, left=None, right=None):
self.val, self.left, self.right = val, left, right
def preorder(node, out):
if node:
out.append(node.val)
preorder(node.left, out)
preorder(node.right, out)
return out
def inorder(node, out):
if node:
inorder(node.left, out)
out.append(node.val)
inorder(node.right, out)
return out
def postorder(node, out):
if node:
postorder(node.left, out)
postorder(node.right, out)
out.append(node.val)
return out
root = Node(1, Node(2, Node(4), Node(5)), Node(3, None, Node(6)))
print(preorder(root, [])) # [1, 2, 4, 5, 3, 6]
print(inorder(root, [])) # [4, 2, 5, 1, 3, 6]
print(postorder(root, [])) # [4, 5, 2, 6, 3, 1]Preorder và inorder dạng lặp với ngăn xếp
Đệ quy dùng ngăn xếp lời gọi. Để tránh giới hạn đệ quy trên cây sâu, hãy tự quản lý ngăn xếp. Với preorder, pop một node, thăm nó, rồi push con phải trước con trái để con trái được xử lý trước.
Với inorder, đi sang trái hết mức có thể, push các node trên đường đi. Khi không thể đi trái nữa, pop một node, thăm nó, rồi chuyển sang con phải của nó.
def preorder_iter(root):
out, stack = [], [root] if root else []
while stack:
node = stack.pop()
out.append(node.val)
if node.right:
stack.append(node.right)
if node.left:
stack.append(node.left)
return out
def inorder_iter(root):
out, stack, node = [], [], root
while stack or node:
while node: # go as far left as possible
stack.append(node)
node = node.left
node = stack.pop()
out.append(node.val)
node = node.right
return out
print(preorder_iter(root)) # [1, 2, 4, 5, 3, 6]
print(inorder_iter(root)) # [4, 2, 5, 1, 3, 6]Postorder và level order dạng lặp
Postorder dạng lặp là khó nhất. Một cách gọn dùng cờ: push mỗi node hai lần, lần thứ hai được đánh dấu là sẵn sàng thăm sau các con của nó. Level order dùng hàng đợi thay vì ngăn xếp; xử lý hàng đợi từng tầng một cũng cho biết độ sâu của mỗi node.
from collections import deque
def postorder_iter(root):
out, stack = [], [(root, False)] if root else []
while stack:
node, children_done = stack.pop()
if children_done:
out.append(node.val)
else:
stack.append((node, True))
if node.right:
stack.append((node.right, False))
if node.left:
stack.append((node.left, False))
return out
def level_order(root):
levels, queue = [], deque([root] if root else [])
while queue:
level = []
for _ in range(len(queue)): # nodes of the current level
node = queue.popleft()
level.append(node.val)
if node.left:
queue.append(node.left)
if node.right:
queue.append(node.right)
levels.append(level)
return levels
print(postorder_iter(root)) # [4, 5, 2, 6, 3, 1]
print(level_order(root)) # [[1], [2, 3], [4, 5, 6]]Một mẹo phổ biến khác cho postorder: chạy preorder đã chỉnh (node, phải, trái) rồi đảo ngược kết quả.
Độ phức tạp thời gian và không gian
| Cách duyệt | Thời gian | Không gian phụ | Lý do |
|---|---|---|---|
| Preorder / inorder / postorder (đệ quy) | O(n) | O(h) | ngăn xếp lời gọi giữ một khung cho mỗi tầng |
| Preorder / inorder / postorder (lặp) | O(n) | O(h) | ngăn xếp tường minh giữ tối đa một đường đi cộng các node anh em |
| Level order | O(n) | O(w) | hàng đợi giữ tối đa một tầng đầy đủ |
| Duyệt inorder Morris | O(n) | O(1) | tạm thời nối các liên kết phải null thành sợi chỉ (thread) |
Ở đây n là số node, h là chiều cao và w là độ rộng tối đa. Với cây cân bằng h khoảng log2(n) và w có thể khoảng n/2; với cây dạng chuỗi h là n và w là 1.
Lỗi thường gặp
- Push con trái trước con phải trong preorder dạng lặp. Ngăn xếp là LIFO, nên con bạn muốn thăm trước phải được push sau cùng.
- Quên cây rỗng: mọi hàm nên trả về kết quả rỗng khi
root is None. - Dùng
listvớipop(0)làm hàng đợi trong level order. Mỗi lần pop tốn O(n); hãy dùngcollections.deque. - Dựa vào đệ quy cho cây rất sâu. Python dừng ở giới hạn đệ quy (mặc định khoảng 1000) với
RecursionError. - Mong một cách duyệt đơn lẻ xác định được cây. Riêng preorder thì mơ hồ; preorder cộng inorder (với giá trị khác nhau) là đủ để dựng lại cây.
Khi nào dùng cách duyệt nào
| Tác vụ | Cách duyệt |
|---|---|
| Liệt kê khóa BST theo thứ tự tăng dần, kiểm tra BST, phần tử nhỏ thứ k | Inorder |
| Sao chép hoặc tuần tự hóa cây, in đại cương thư mục | Preorder |
| Xóa cây, tính kích thước hoặc chiều cao cây con, tính cây biểu thức | Postorder |
| Độ sâu nhỏ nhất, góc nhìn bên phải, in theo tầng, node gần nhất | Level order |
Preorder, inorder và postorder đều là các dạng tìm kiếm theo chiều sâu; level order là tìm kiếm theo chiều rộng áp dụng cho cây. Cùng ý tưởng này mở rộng cho đồ thị tổng quát, nơi bạn còn cần một tập các node đã thăm.
Câu hỏi thường gặp
Preorder, inorder và postorder khác nhau thế nào?
Chỉ khác vị trí thăm node. Preorder thăm node trước các cây con, inorder thăm giữa cây con trái và phải, và postorder thăm sau cả hai cây con.
Cách duyệt nào cho kết quả đã sắp xếp với cây tìm kiếm nhị phân?
Duyệt inorder. Trong BST mọi khóa bên trái nhỏ hơn và mọi khóa bên phải lớn hơn, nên thăm trái, node, phải cho ra các khóa theo thứ tự tăng dần.
Duyệt level-order có giống BFS không?
Có. Duyệt level-order là tìm kiếm theo chiều rộng trên cây. Nó dùng hàng đợi và thăm các node theo thứ tự độ sâu.
Tại sao dùng duyệt dạng lặp thay vì đệ quy?
Độ sâu đệ quy bằng chiều cao cây, và Python báo RecursionError khi vượt giới hạn đệ quy. Ngăn xếp tường minh không có giới hạn đó và giúp dễ dàng tạm dừng hoặc dừng sớm.
Có thể duyệt cây với O(1) không gian phụ không?
Có. Duyệt Morris tạm thời nối tiền nhiệm inorder của mỗi node trỏ ngược về nó, nên không cần ngăn xếp, và khôi phục cây trước khi kết thúc. Thời gian chạy là O(n).