cv.cm

Lập trình · Cấu trúc dữ liệu

Duyệt cây nhị phân: preorder, inorder, postorder và level order

6 phút đọc

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ắcKết quả
Preordernode, trái, phải1 2 4 5 3 6
Inordertrái, node, phải4 2 5 1 3 6
Postordertrái, phải, node4 5 2 6 3 1
Level ordertừng hàng, từ trái sang phải1 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ệtThời gianKhô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 orderO(n)O(w)hàng đợi giữ tối đa một tầng đầy đủ
Duyệt inorder MorrisO(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 list với pop(0) làm hàng đợi trong level order. Mỗi lần pop tốn O(n); hãy dùng collections.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ứ kInorder
Sao chép hoặc tuần tự hóa cây, in đại cương thư mụcPreorder
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ứcPostorder
Độ sâu nhỏ nhất, góc nhìn bên phải, in theo tầng, node gần nhấtLevel 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).

Tất cả hướng dẫn lập trình