นากสีมิ้นต์ต้นฉบับกำลังจัดลูกแก้วจากต้นไม้ลงถาดแนวนอนทีละระดับบนโต๊ะสว่าง

LeetCode 102 Level Order Traversal: เลือก queue, index หรือ DFS

พี่โชว์Engineering

โจทย์ให้ binary tree แล้วต้องคืนค่าของ node แยกเป็นรายการตามระดับจาก root ลงมาและซ้ายไปขวาครับ คำตอบธรรมชาติคือ BFS แต่ยังมีรายละเอียดว่า queue แบบใดและจะจัด boundary ของแต่ละ level อย่างไร

WHY: frontier คือ state หลัก

BFS เก็บ node ที่รอเยี่ยมใน queue เมื่อเริ่มรอบหนึ่ง ขนาด queue คือจำนวน node ของ level ปัจจุบัน เราจึง pop เท่านั้นแล้ว append ลูกสำหรับรอบถัดไป วิธีนี้ O(n) time และ O(w) memory โดย w คือความกว้างสูงสุดของต้นไม้

list.pop(0) ดูง่ายแต่เลื่อนสมาชิกทุกครั้งจนเสี่ยง O(n²) ใช้ collections.deque หรือ list ที่มี head index เพื่อให้การนำหน้าออกเป็น O(1) amortized

HOW: ทดสอบสามแนวคิดกับ tree เดียวกัน

import unittest
from collections import deque
from dataclasses import dataclass


@dataclass
class Node:
    value: int
    left: "Node | None" = None
    right: "Node | None" = None


def levels_deque(root):
    if root is None:
        return []
    queue, result = deque([root]), []
    while queue:
        level = []
        for _ in range(len(queue)):
            node = queue.popleft()
            level.append(node.value)
            if node.left: queue.append(node.left)
            if node.right: queue.append(node.right)
        result.append(level)
    return result


def levels_index(root):
    if root is None:
        return []
    queue, head, result = [root], 0, []
    while head < len(queue):
        end, level = len(queue), []
        while head < end:
            node = queue[head]
            head += 1
            level.append(node.value)
            if node.left: queue.append(node.left)
            if node.right: queue.append(node.right)
        result.append(level)
    return result


def levels_dfs(root):
    result = []
    def visit(node, depth):
        if node is None: return
        if depth == len(result): result.append([])
        result[depth].append(node.value)
        visit(node.left, depth + 1)
        visit(node.right, depth + 1)
    visit(root, 0)
    return result


class LevelTests(unittest.TestCase):
    def test_methods(self):
        tree = Node(3, Node(9), Node(20, Node(15), Node(7)))
        expected = [[3], [9, 20], [15, 7]]
        for function in (levels_deque, levels_index, levels_dfs):
            self.assertEqual(function(tree), expected)
            self.assertEqual(function(None), [])


if __name__ == "__main__":
    unittest.main()

ตารางตัดสินใจ

เงื่อนไขวิธีเวลาauxiliary memoryข้อแลกเปลี่ยน
default/readabilitydeque BFSO(n)O(w)level boundary ชัด
ไม่อยาก import dequelist + headO(n)O(n)list เก็บ node ที่ผ่านแล้ว
tree ไม่ลึกและโค้ด DFS มีอยู่DFS by depthO(n)O(h)เสี่ยง recursion limit

best default คือ deque BFS เพราะตรงกับคำว่า level order และ memory สัมพันธ์กับ frontier จริง DFS ใช้ได้แต่ต้นไม้เอียงลึกอาจชน recursion limit ส่วน list+head เร็วแต่ไม่คืน memory ด้านหน้าจนจบ

ถ้าข้อมูลเป็น stream ของ edges ที่ยังสร้าง tree ไม่ครบ ต้องกำหนด ordering และ storage เพิ่ม ไม่ใช่โยน iterator เข้า algorithm นี้ตรง ๆ edge cases ได้แก่ root ว่าง, node เดียว, tree เอียง และ level กว้างมาก

Recap

  • จับขนาด queue ก่อนเริ่ม level เพื่อกำหนด boundary
  • ใช้ deque แทน pop(0)
  • DFS สะสมตาม depth ได้แต่แลกกับ call stack
  • memory ของ BFS ขึ้นกับความกว้าง ส่วน DFS ขึ้นกับความสูง
  • เลือก deque BFS เป็น default ที่อ่าน intent ชัดที่สุด

ลองเพิ่ม test tree เอียงหนึ่งหมื่นระดับเพื่อเห็นความต่างของ recursion กับ iterative queue ด้วยตัวเองครับ