🚩传送门:牛客题目
力扣题目

题目

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

二叉树的一棵子树包括某个节点和这个节点的所有后代节点

示例 1:
[NC]98 另一棵树的子树(完全相同) - 图1

输入:root = [3,4,5,1,2], subRoot = [4,1,2] 输出:true

示例 2:
[NC]98 另一棵树的子树(完全相同) - 图2

输入:root = [3,4,5,1,2,null,null,null,null,0], subRoot = [4,1,2] 输出:false

解题思路:深度优先搜索暴力匹配

这是一种最朴素的方法——深度优先搜索枚举 s中的每一个节点作为根节点与子树进行匹配,判断这个点作为根节点其子树是否和t相等。如何判断一个节点的子树是否和t相等呢,我们又需要做一次深度优先搜索来检查,即让两个指针一开始先指向该节点和 t 的根,然后「同步移动」两根指针来「同步遍历」这两棵树,判断对应位置是否相等。

复杂度分析

时间复杂度:[NC]98 另一棵树的子树(完全相同) - 图3,对于每一个 [NC]98 另一棵树的子树(完全相同) - 图4 上的点,都需要做一次深度优先搜索来和 [NC]98 另一棵树的子树(完全相同) - 图5 匹配,匹配一次
的时间代价是 [NC]98 另一棵树的子树(完全相同) - 图6,那么总的时间代价就是[NC]98 另一棵树的子树(完全相同) - 图7。故渐进时间复杂度为[NC]98 另一棵树的子树(完全相同) - 图8

空间复杂度:[NC]98 另一棵树的子树(完全相同) - 图9,假设 [NC]98 另一棵树的子树(完全相同) - 图10 深度为 [NC]98 另一棵树的子树(完全相同) - 图11 ,假设 [NC]98 另一棵树的子树(完全相同) - 图12 深度为 [NC]98 另一棵树的子树(完全相同) - 图13 ,任意时刻栈空间的最大使用
代价是[NC]98 另一棵树的子树(完全相同) - 图14,故渐进空间复杂度为[NC]98 另一棵树的子树(完全相同) - 图15

我的代码

  1. class Solution {
  2. public boolean isSubtree(TreeNode s, TreeNode t) {
  3. return dfs(s, t);
  4. }
  5. public boolean dfs(TreeNode s, TreeNode t) {
  6. if (s == null) return false;
  7. if (check(s, t)) return true; // 1. 检查当前结点为根是否可行
  8. return dfs(s.left, t) || dfs(s.right, t); // 2. 递归左右子树
  9. }
  10. public boolean check(TreeNode s, TreeNode t) {
  11. if (s == null && t == null) {
  12. return true;
  13. }
  14. if (s == null || t == null || s.val != t.val) {
  15. return false;
  16. }
  17. return check(s.left, t.left) && check(s.right, t.right);
  18. }
  19. }

解题思路:深度优先搜索序列上做串匹配

这个方法需要我们先了解一个「常识」:一棵子树上的点在先序遍历中是连续的。我们可以确定解决这个问题的方向就是:把st先转换成深度优先搜索序列(先序遍历序列),然后看t的深度优先搜索序列是否是s的深度优先搜索序列的「子串」。

这样做正确吗?如下图可示
[NC]98 另一棵树的子树(完全相同) - 图16
由此可见「s 的深度优先搜索序列包含t的深度优先搜索序列」是「ts子树」的必要不充分条件,所以单纯这样做是不正确的。

34125 包含了字串的 41 ,但显然不是其子串

🚩为了解决这个问题,我们可以引入两个空值 lNullrNull,当一个节点的左孩子或者右孩子为空的时候,就插入这两个空值,这样深度优先搜索序列就唯一对应一棵树。处理完之后,就可以通过判断「s 的深度优先搜索序列包含 t 的深度优先搜索序列」来判断答案。

在判断「s 的深度优先搜索序列包含 t 的深度优先搜索序列」的时候,可以暴力匹配,也可以使用 KMP 或者 Rabin-Karp 算法,在使用 Rabin-Karp 算法的时候,要注意串中可能有负值。这里给出用 KMP 判断的代码实现。

复杂度分析

时间复杂度:[NC]98 另一棵树的子树(完全相同) - 图17,遍历两棵树得到深度优先搜索序列的时间代价是[NC]98 另一棵树的子树(完全相同) - 图18,在匹配的时
候,如果使用暴力匹配,时间代价为[NC]98 另一棵树的子树(完全相同) - 图19,使用 [NC]98 另一棵树的子树(完全相同) - 图20[NC]98 另一棵树的子树(完全相同) - 图21 进行串匹配的时间代
价都是 [NC]98 另一棵树的子树(完全相同) - 图22,由于这里的代码使用 [NC]98 另一棵树的子树(完全相同) - 图23 实现的,所以渐进时间复杂度为[NC]98 另一棵树的子树(完全相同) - 图24

空间复杂度:[NC]98 另一棵树的子树(完全相同) - 图25,这里保存了两个深度优先搜索序列,还计算了[NC]98 另一棵树的子树(完全相同) - 图26长度的 [NC]98 另一棵树的子树(完全相同) - 图27 数组,
辅助空间的总代价为 [NC]98 另一棵树的子树(完全相同) - 图28,任意时刻栈空间的最大使用代价是[NC]98 另一棵树的子树(完全相同) - 图29,然而由于这
[NC]98 另一棵树的子树(完全相同) - 图30,故渐进空间复杂度为 [NC]98 另一棵树的子树(完全相同) - 图31

🚩我的代码

  1. public class Solution {
  2. public boolean isContains (TreeNode root1, TreeNode root2) {
  3. // write code here
  4. StringBuffer res1=new StringBuffer();
  5. StringBuffer res2=new StringBuffer();
  6. preOrder(root1,res1);
  7. preOrder(root2,res2);
  8. if(res1.toString().contains(res2.toString()))return true; // 重点调用 contains
  9. return false;
  10. }
  11. public void preOrder(TreeNode root,StringBuffer res){
  12. if(root==null)return;
  13. res.append(root.val);
  14. if(root.left==null) res.append("#1");
  15. else preOrder(root.left,res);
  16. if(root.right==null) res.append("#2");
  17. else preOrder(root.right,res);
  18. }
  19. }

😱原封代码

需要自己手撸KMP的原装代码,人嘛了。

  1. class Solution {
  2. int lNull, rNull;
  3. public boolean isSubtree(TreeNode s, TreeNode t) {
  4. List<Integer> sOrder = new ArrayList<Integer>(); // 存储s先序遍历结果
  5. List<Integer> tOrder = new ArrayList<Integer>(); // 存储t先序遍历结果
  6. lNull = Integer.MAX_VALUE - 1; // 左子树为空标记符
  7. rNull = Integer.MAX_VALUE; // 右子树为空标记符
  8. getDfsOrder(s, sOrder); // 获取 s 的先序遍历序列
  9. getDfsOrder(t, tOrder); // 获取 t 的先序遍历序列
  10. return kmp(); // 返回二者 Kmp 的比较结果
  11. }
  12. public void getDfsOrder(TreeNode t, List<Integer> preOrder) {
  13. if (t == null) return;
  14. preOrder.add(t.val); // 1. 添加当前结点
  15. if (t.left != null) getDfsOrder(t.left, preOrder); // 2. 左子树不空递归左子树
  16. else preOrder.add(lNull); // 2. 添加 lNull 标识符
  17. if (t.right != null) getDfsOrder(t.right, preOrder);// 3. 右子树不空递归右子树
  18. else preOrder.add(rNull); // 3. 添加 rNull 标识符
  19. }
  20. public boolean kmp(List<Integer> sOrder,List<Integer> tOrder) {
  21. int sLen = sOrder.size(), tLen = tOrder.size();
  22. int[] fail = new int[tOrder.size()];
  23. Arrays.fill(fail, -1);
  24. for (int i = 1, j = -1; i < tLen; ++i) {
  25. while (j != -1 && !(tOrder.get(i).equals(tOrder.get(j + 1)))) {
  26. j = fail[j];
  27. }
  28. if (tOrder.get(i).equals(tOrder.get(j + 1))) {
  29. ++j;
  30. }
  31. fail[i] = j;
  32. }
  33. for (int i = 0, j = -1; i < sLen; ++i) {
  34. while (j != -1 && !(sOrder.get(i).equals(tOrder.get(j + 1)))) {
  35. j = fail[j];
  36. }
  37. if (sOrder.get(i).equals(tOrder.get(j + 1))) {
  38. ++j;
  39. }
  40. if (j == tLen - 1) {
  41. return true;
  42. }
  43. }
  44. return false;
  45. }
  46. }