Description

給定一棵二元樹的根節點,找出所有重複的子樹。兩棵子樹重複意味著它們擁有相同的結構和節點值。對每種重複的子樹,只需回傳其中一棵的根節點。

Example:

Input: root = [1,2,3,4,null,2,4,null,null,4] Output: [[2,4],[4]] (子樹 [2,4] 和子樹 [4] 各出現了兩次)

Intuition#

將每棵子樹序列化為字串,用 HashMap 記錄出現次數,出現第二次時加入結果。

  • 要判斷子樹是否相同,最直接的方式是將子樹序列化成唯一的字串表示
  • 用 HashMap 記錄每種序列化字串出現的次數
  • 當某個序列化字串第二次出現時,將該子樹的根加入結果

Approaches#

1: 序列化 + HashMap#

  • 概念: 後序遍歷,將每棵子樹序列化為字串,用 HashMap 計數
  • 時間複雜度: O(n^2),每個節點的序列化字串長度可達 O(n)
  • 空間複雜度: O(n^2),存放所有序列化字串
class Solution {
    fun findDuplicateSubtrees(root: TreeNode?): List<TreeNode?> {
        val result = mutableListOf<TreeNode?>()
        val map = HashMap<String, Int>()
        fun serialize(node: TreeNode?): String {
            if (node == null) return "#"
            val s = "${node.`val`},${serialize(node.left)},${serialize(node.right)}"
            map[s] = (map[s] ?: 0) + 1
            if (map[s] == 2) result.add(node)
            return s
        }
        serialize(root)
        return result
    }
}

⭐ 2: 序列化 + ID 映射優化#

  • 概念: 為每種子樹結構分配唯一 ID(整數),避免重複拼接長字串
  • 時間複雜度: O(n),每個節點只做 O(1) 的 triple 比較和 ID 查找
  • 空間複雜度: O(n)
class Solution {
    fun findDuplicateSubtrees(root: TreeNode?): List<TreeNode?> {
        val result = mutableListOf<TreeNode?>()
        val tripleToId = HashMap<Triple<Int, Int, Int>, Int>()
        val idCount = HashMap<Int, Int>()
        var nextId = 1

        fun dfs(node: TreeNode?): Int {
            if (node == null) return 0
            val leftId = dfs(node.left)
            val rightId = dfs(node.right)
            val triple = Triple(node.`val`, leftId, rightId)
            val id = tripleToId.getOrPut(triple) { nextId++ }
            idCount[id] = (idCount[id] ?: 0) + 1
            if (idCount[id] == 2) result.add(node)
            return id
        }

        dfs(root)
        return result
    }
}

🔑 Takeaways#

  • Pattern: 子樹序列化 — 將樹結構轉換為可比較的表示形式
  • 關鍵技巧: 用 ID 映射取代字串拼接可以將時間複雜度從 O(n^2) 降到 O(n),核心思路是為每種子樹結構分配唯一整數 ID