二叉树子树判断:从递归到迭代的全方位解析
一、题目解析
题目描述
给定两棵二叉树root
和subRoot
,判断root
中是否存在一棵子树,其结构和节点值与subRoot
完全相同。
示例说明
-
示例1:
root = [3,4,5,1,2]
,subRoot = [4,1,2]
返回true
,因为root
的左子树与subRoot
完全相同。 -
示例2:
root = [3,4,5,1,2,null,null,null,null,0]
,subRoot = [4,1,2]
返回false
,虽然root
存在节点值为4、1、2的路径,但结构与subRoot
不同。
核心难点
- 如何高效遍历主树,找到可能与子树匹配的起始节点?
- 如何精确判断两棵树是否完全相同,避免结构或值的差异?
二、递归解法:深度优先搜索
核心思路
- 递归遍历主树:对主树的每个节点,检查以该节点为根的子树是否与目标子树相同。
- 递归判断子树是否相同:同时遍历两棵树的对应节点,确保结构和值完全一致。
代码实现
class Solution {public boolean isSubtree(TreeNode root, TreeNode subRoot) {return bfs(root,subRoot);}public boolean bfs(TreeNode root, TreeNode subRoot){if(root == null){return false;}return check(root,subRoot) || bfs(root.left,subRoot) || bfs(root.right,subRoot);}public boolean check(TreeNode root, TreeNode subRoot){if(root == null && subRoot == null){return true;}if(root == null || subRoot == null || root.val != subRoot.val){return false;}return check(root.left,subRoot.left) && check(root.right,subRoot.right);}
}
代码解析
-
主方法
isSubtree
:
调用bfs
方法开始递归遍历主树。 -
递归遍历方法
bfs
:- 终止条件:若主树为空,返回
false
。 - 检查当前节点:调用
check
方法判断以当前节点为根的子树是否与目标子树相同。 - 递归搜索左右子树:只要在任一子树中找到匹配,立即返回
true
。
- 终止条件:若主树为空,返回
-
子树比较方法
check
:- 终止条件:若两节点均为空,返回
true
;若仅一者为空或值不同,返回false
。 - 递归比较:递归检查左右子树是否完全相同。
- 终止条件:若两节点均为空,返回
三、迭代解法:双端队列层序遍历
核心思路
- 层序遍历主树:使用双端队列按层遍历主树的每个节点。
- 检查子树是否相同:对每个节点,使用另一个双端队列同步比较其与目标子树的结构和值。
代码实现
class Solution {public boolean isSubtree(TreeNode root, TreeNode subRoot) {Deque<TreeNode> cur = new LinkedList<>();if (subRoot == null) return true; if (root == null) return false; TreeNode tempRoot = root;cur.offer(tempRoot);while(!cur.isEmpty()){tempRoot = cur.pollFirst();if(isSameTree(tempRoot,subRoot)){return true;}if(tempRoot.left != null){cur.offerFirst(tempRoot.left);}if(tempRoot.right != null){cur.offerFirst(tempRoot.right);}}return false;}public boolean isSameTree(TreeNode root, TreeNode subRoot) {Deque<TreeNode> cur = new LinkedList<>();if (root == null && subRoot == null) {return true;}TreeNode tempRoot = root;TreeNode tempSubRoot = subRoot;cur.offerFirst(tempRoot);cur.offerLast(tempSubRoot);while (!cur.isEmpty()) {tempRoot = cur.pollFirst();tempSubRoot = cur.pollLast();if (tempRoot == null && tempSubRoot == null) {continue;}if (tempRoot == null || tempSubRoot == null || tempSubRoot.val != tempRoot.val) {return false;}cur.offerFirst(tempRoot.left);cur.offerFirst(tempRoot.right);cur.offerLast(tempSubRoot.left);cur.offerLast(tempSubRoot.right);}return true;}
}
代码解析
-
主方法
isSubtree
:- 初始化队列:将主树根节点入队。
- 层序遍历:每次从队列取出节点,调用
isSameTree
检查是否匹配。 - 扩展队列:将当前节点的左右子节点入队,继续遍历。
-
子树比较方法
isSameTree
:- 双队列同步遍历:使用双端队列分别存储主树和子树的节点。
- 节点比较:每次从队列取出两个节点,检查是否结构和值相同,并将其子节点入队。
- 入队策略:主树节点从队首入队,子树节点从队尾入队,确保对应节点同步比较。
四、寻找匹配节点的关键逻辑
递归法
- 遍历策略:采用前序遍历(根→左→右),确保每个节点都被检查。
- 匹配条件:当且仅当当前节点及其所有后代与子树完全相同时,返回
true
。
迭代法
- 遍历策略:层序遍历(BFS),按层检查每个节点。
- 匹配条件:使用双队列同步比较,确保每个对应节点的结构和值一致。
对比分析
方法 | 遍历方式 | 时间复杂度 | 空间复杂度 | 适用场景 |
---|---|---|---|---|
递归法 | 深度优先(DFS) | O(m*n) | O(max(h1,h2)) | 树较浅,递归栈空间充足 |
迭代法 | 广度优先(BFS) | O(m*n) | O(max(w1,w2)) | 避免递归栈溢出 |
其中,m和n分别为主树和子树的节点数,h为树的高度,w为树的最大宽度。
五、常见误区与边界条件
1. 空树处理
- 子树为空:题目规定空树是任何树的子树,因此直接返回
true
。 - 主树为空:若主树为空,仅当子树也为空时返回
true
,否则返回false
。
2. 结构与值的双重匹配
- 示例:主树
[1,1]
,子树[1]
。
虽然主树存在节点值为1的子树,但结构不同(主树有两个节点),因此返回false
。
3. 部分路径匹配问题
- 示例:主树
[3,4,5,1,2,null,null,null,null,0]
,子树[4,1,2]
。
主树的左子树路径为4→1→2
,但2
的右子节点存在0
,与子树结构不符,故返回false
。
六、总结
核心算法思路
- 遍历主树:递归或迭代方式遍历每个节点。
- 检查子树:对每个节点,递归或迭代比较其与目标子树的结构和值。
代码优化建议
- 递归法:简洁直观,但可能导致栈溢出,适用于树高较小的场景。
- 迭代法:避免栈溢出,但代码复杂度较高,需维护双队列同步遍历。
关键技巧
- 双队列同步遍历:确保主树和子树的对应节点被正确比较。
- 层序遍历:通过队列按层处理节点,适合大规模数据。
理解这两种解法的核心逻辑后,可灵活应对类似的树结构匹配问题,如判断树的子结构、树的镜像等变体。