572. 另一棵树的子树(Subtree of Another Tree)

难度:简单 | 主题:二叉树、DFS、递归

题目

给你两棵二叉树 root 和 subRoot。判断 root 中是否包含一棵和 subRoot 结构相同、节点值相同的子树。如果存在,返回 true;否则返回 false。

示例

 
输入:root = [3,4,5,1,2], subRoot = [4,1,2]
 
输出:true
 
解释:root 中以 4 为根的子树和 subRoot 完全一致
 

思路

先讲个故事:拼图里找碎片

想象你有一幅巨大的拼图(root),手里拿着一个小碎片(subRoot)。

你要判断:这个碎片是不是大拼图中完整的一块?

这需要两步:

  1. 在大拼图里找到一个起点,看起来和小碎片的图案可能匹配

  2. 从这个起点出发,逐块比较,确认每一块都对得上

对应到树上也是两步:遍历找候选根 + 递归比较是否相同


引导式推导:从单点到双递归

第 1 步:怎么判断两棵树完全相同?

100相同的树 告诉我们:

 
check(o, t):
 
  都空 → True
 
  一空一不空 / 值不同 → False
 
  递归比较左右子树
 

第 2 步:怎么找候选根?

对 root 的每个节点,都问一句:“以你为根的子树和 subRoot 一样吗?”

 
isSubtree(root, subRoot):
 
  root 为空 → False(没地方找了)
 
  root 和 subRoot 相同?→ True(找到了!)
 
  在左子树里找 → isSubtree(root.left, subRoot)
 
  在右子树里找 → isSubtree(root.right, subRoot)
 

第 3 步:双递归的嵌套

这就是经典的双递归模式:

  • 外层递归(isSubtree):在大树上遍历每个节点

  • 内层递归(check / isSameTree):在固定节点上比较两棵树

 
    外层:找起点                  内层:逐点校验
 
┌─────────────┐           ┌────────────────────┐
 
│ 遍历每个节点 │──候选根──→│ 比较两棵树是否相同  │
 
│ isSubtree   │           │ check(sameTree)    │
 
└─────────────┘           └────────────────────┘
 

代码

 
def isSubtree(self, s, t):
 
    # s: 大树,t: 子树模板
 
    # 遍历 s 的每个节点,尝试匹配 t
 
    if not s:                    # 大树遍历到空了还没找到
 
        return False
 
    # 三路或:当前节点匹配 OR 左子树里有 OR 右子树里有
 
    return (self.check(s, t) or
 
            self.isSubtree(s.left, t) or
 
            self.isSubtree(s.right, t))
 
def check(self, o, t):
 
    # 逐节点比较两棵树是否完全相同
 
    if not o and not t:          # 同时遍历完 → 完全匹配
 
        return True
 
    if not o or not t or o.val != t.val:  # 结构不同或值不同 → 不匹配
 
        return False
 
    # 左右子树必须同时完全相同
 
    return self.check(o.left, t.left) and self.check(o.right, t.right)
 

为什么三路用 or

只要有一条路找到就行。找到 → 直接返回 True,不用继续找了。

如果当前节点不匹配,往左子树找 OR 往右子树找,两个方向有一个成功就行。


复杂度

解法时间空间
DFS 遍历 + 比较O(s
序列化 + KMP(进阶)O(s

最坏情况:每个 s 节点都要和 t 完整比较一次(比如 s 全是相同值的节点,t 是单节点树),复杂度退化为 O(n×m)。


实战考量

频率分析

出现在:字节/美团 二叉树递归考察题,实践中频。考察对递归嵌套的理解,比单纯遍历多一层抽象。

延伸思考

Q:为什么不用写 isSubtree(s, t) 的边界条件是 if not s and not t: return True

A:题目保证 t 非空。s 为空 t 非空 → 不是子树,返回 False。且 t 如果是空树(题目不会给),按定义任何树都有空子树,应该返回 True。但本题约定 t 非空。

Q:序列化 + KMP 的思路是什么?

A:把两棵树序列化成字符串(前序/后序,用特殊标记表示空节点),问题转化为”subRoot 的序列是否是 root 序列的子串”。KMP O(n+m) 匹配。提出来可以加分,但手写递归足够。

Q:外层递归的最坏复杂度?

A:每个 s 节点都可能触发一次完整的 check。如果 s 退化成链表且每个节点值都等于 t 的根值,那么每个节点都要和 t 完整比较,O(n×m)。

Q:如果 root 中有多个匹配呢?

A:找到一个就返回 True,不会继续找。这是三路 or 的短路特性。

易错点

  • 外层 or 逻辑:不能写成 and——三路是”有一条路通就行”

  • check 的空判断:先判断同时空(True),再判断一空一不空(False)。顺序不能反,否则 if not o and not t 先被 not o or not t 拦截了

  • isSubtree 的空处理:if not s: return False——s 空了但 t 非空,不是子树

  • 值和结构都要比较,只比较结构(101对称二叉树)或只比较值都不够


生活类比

找子树 → 在拼图里找完整碎片

你有一块小碎片(subRoot),要在大拼图(root)里找。

外层循环(isSubtree)是”扫描——这里看起来像起点吗?”

内层比较(check)是”逐一比对——这块对齐吗?下一块呢?”

两件事都得做:不扫描找不到起点,不比对比不知道是不是真的一样。


相关题目

题目关系
100相同的树check 函数的来源,完全相同的问题
104二叉树的最大深度二叉树递归遍历基础
543二叉树的直径同是后序 DFS 遍历框架
101对称二叉树比较两棵树的对称版本

→ 返回题单:LeetCode学习路线图 > 五、二叉树

速记卡(面试闪卡)

Q1:一句话讲清「572. 另一棵树的子树(Subtree of Another Tree)」到底是什么?

A:判子树用双递归:外层遍历找候选根,内层逐点比两树是否相同。

Q2:一、题目与拼图碎片类比 —— 怎么理解?

A:像在大拼图里找完整小碎片:先在大图里找一个”看起来像起点”的位置,再从这出发逐块比对,两步缺一不可。英文:Subtree Matching。

Q3:二、双递归:isSubtree 套 check —— 怎么理解?

A:像两层排查:外层 isSubtree 遍历大树每个节点当候选根,内层 check 固定起点逐点比两棵树——典型的”遍历+校验”嵌套。英文:Double Recursion。

Q4:三、为什么三路用 or —— 怎么理解?

A:像三条路只要一条通:当前节点匹配、或左子树里有、或右子树里有,任一为真即返回 True,利用短路早停——写成 and 就错了。英文:OR Short-circuit。

Q5:四、复杂度与进阶 KMP —— 怎么理解?

A:像最坏每点都比一遍:时间 O(|s|×|t|)、空间 O(深度);进阶把两树序列化再 KMP 子串匹配可降到 O(|s|+|t|),加分但不必手写。英文:DFS / Serialize+KMP。

Q6:核心速记主线有哪些?

  • 双递归:外层 isSubtree 遍历、内层 check 比同树

  • 三路 or:当前 / 左 / 右任一匹配即 True

  • 复杂度:时间 O(|s|×|t|)、空间 O(最大深度)

  • 进阶:序列化+KMP 可优化到 O(|s|+|t|)

口诀

A:子树拼图慢慢认,

外层找根内层问;

三路或通便为真,

双递归嵌套稳。

相关链接