← 返回题目列表

如何找到二叉树中的重复子树?为什么常用子树序列化做签名?

中等 第 25 / 30 题 更新于 2026/07/30
二叉树重复子树序列化哈希

简化版

找重复子树可以给每棵子树生成唯一签名,比如“根值 + 左子树签名 + 右子树签名”,再用哈希表统计签名出现次数。某个签名第二次出现时,就说明找到一类重复子树。

详细版

重复子树要求结构相同、节点值也相同。常用后序 DFS:

  • 空节点返回特殊标记 #
  • 左右子树先生成签名。
  • 当前签名为 val,leftSig,rightSig
  • 用 Map 记录签名出现次数。
  • 当次数从 1 变成 2 时,把当前根加入答案,避免重复加入同一类。

后序很自然,因为当前子树签名依赖左右子树签名。注意签名必须包含空节点标记,否则不同结构可能被误判相同。

完整版教学

一、重复子树的判定包含两件事

重复子树不是只看根节点值相同,也不是只看节点数量相同。它要求结构相同,并且对应位置的节点值相同。

例如:

  2       2
 /         \
3           3

这两棵子树节点值集合一样,但结构不同,不应该算重复。签名必须同时编码值和结构。

二、为什么后序遍历适合生成签名

当前子树的签名依赖左子树和右子树。只有先知道左右签名,才能拼出当前签名。

sig(node) = node.val + "," + sig(node.left) + "," + sig(node.right)

这正是后序遍历:左、右、根。后序不是随便选的,而是由依赖关系决定的。父节点要等孩子信息都准备好。

三、为什么空节点标记不能省

如果省略空节点,结构可能混淆。比如一个节点只有左孩子,和一个节点只有右孩子,字符串可能被拼得很像。

正确签名要包含 #

只有左孩子:1,2,#,#,#
只有右孩子:1,#,2,#,#

空标记让结构边界明确。序列化树时常说“空节点也要编码”,这里原因一样。

四、为什么只在第二次出现时加入答案

如果同一种重复子树出现 4 次,答案通常只需要返回一个代表根。如果每次出现都加入,会重复输出。

逻辑是:

count[sig] += 1
if (count[sig] === 2) ans.push(node)

第一次出现只是建立基准;第二次出现说明确实重复;第三次以后已经报告过这一类,不需要再加。

五、字符串签名的成本和优化

直接拼字符串容易理解,但大树上字符串总长度可能较大。可以把每种三元组 (val,leftId,rightId) 映射成整数 id,用 id 作为子树签名。

方式优点缺点
字符串签名简单直观字符串拼接可能较重
整数 ID更省空间实现更复杂
哈希签名要考虑碰撞

面试中先讲字符串版,再补充 ID 优化,会显得层次很清楚。

六、复杂度怎么分析

每个节点访问一次。若用整数 ID,时间 O(n),空间 O(n)。若用字符串,理论上拼接和存储可能让总成本高于 O(n),但面试基础版通常接受 O(n) 节点遍历的描述,并补充字符串成本。

记忆钩子:重复子树就是给每棵子树办“身份证”;身份证必须写上根值、左子树、右子树和空节点。

七、常见误区与追问

  • 误区:只比较根节点值就能判断重复。 还要比较完整结构和所有对应节点值。
  • 误区:空节点标记可以省略。 省略会让不同结构产生相同签名。
  • 误区:每次重复都加入答案。 通常只在出现次数等于 2 时加入,避免同类重复。
  • 追问:为什么用后序? 当前签名依赖左右子树签名,所以必须先处理孩子。
  • 追问:字符串太慢怎么办? 用三元组映射整数 ID,减少重复字符串拼接。

八、加强记忆

找重复子树的核心是“子树签名”。后序先拿左右签名,再拼当前签名;哈希表统计签名次数;第二次出现时记录答案。空节点标记是结构正确性的保险,不能省。