剑指Offer_#26_树的子结构

剑指Offer_#26_树的子结构

Contents

题目

输入两棵二叉树A和B,判断B是不是A的子结构。(约定空树不是任意一个树的子结构)
B是A的子结构, 即 A中有出现和B相同的结构和节点值。

示例 1:

输入:A = [1,2,3], B = [3,1]
输出:false

示例 2:

输入:A = [3,4,5,1,2], B = [4,1]
输出:true

限制:

0 <= 节点个数 <= 10000

思路分析

题意分析

题意有一点模糊的地方,就是没讲清楚输入数组的形式代表的是哪种遍历的序列,经过测试,这个代表的是广度优先遍历的序列,也就是按照从上到下,从左到右的顺序进行遍历。

思路

主要分为两个步骤,
1.第一个步骤是找到A当中与B的根节点相同的节点
2.第二个步骤是从这个相同的节点出发,判断两个节点的子树的结构是否相同

两个步骤都可以通过递归进行前序遍历,分别分析两个递归代码。

步骤1:遍历A寻找相同节点

  • 出口条件:
    • A和B其中一个是null,返回false
      • A是null:说明将整个父树遍历完了,还没有找到与B相同的子结构
      • B是null:注意到递归过程中,B是null,说明输入的B就是null,根据题目中条件,空树不是任意一个树的子结构,直接返回false
    • A.val == B.val,说明找到了与A相同的节点,调用判断子树是否相同的函数进一步判断,如果结果为true,就返回true
  • 递推过程:
    • 继续判断从A的左子节点和右子节点出发,子树结构是否和B相同。返回二者的或。

步骤2:遍历AB判断子树结构是否相同

写法1:逆向思维,如果从来没有不相同的节点,所有节点都相同
  • 出口条件

    • B已经遍历完成,即遇到B == null,返回true (隐含的逻辑是:之前一直没有返回false,才可以到这一步,所以之前的所有对应节点都是相同的)
    • A已经遍历完成且B还没遍历完,子树结构不可能相同,返回false
    • A和B的值不同,子树结构不可能相同,返回false
  • 递推过程

    • 判断A的左右子树与B的左右子树结构是否相同。
写法2:正向思维,如果每个节点都相同,那么所有节点相同

与写法1的区别在于,这里边有一个“与”的逻辑,也就是说,每一次递归调用时,A与B的节点都相同,才可以保证所有节点都相同。
这样写是比较麻烦的,因为这意味着,需要单独维护一个全局变量isSubTreeSame,在每个递归过程中更新这个变量。
相比写法1修改的地方

  • 出口条件:
    • 当B遍历结束时,返回isSubTreeSame
  • 递推过程:
    • 更新变量isSubTreeSameisSubTreeSame = (isSubTreeSame && (A.val == B.val));

解答

解答1:逆向思维

class Solution {
    public boolean isSubStructure(TreeNode A, TreeNode B) {
        if(A == null || B == null) return false;
        if(A.val == B.val){//在A中寻找B的根节点,找到后就继续判断此根节点的子树是否相同
            if(isSubTreeSame(A,B)) return true;
        }
        return isSubStructure(A.left,B) || isSubStructure(A.right,B);
    }
    //输入两个根节点,判断子树结构是否相同
    public boolean isSubTreeSame(TreeNode A,TreeNode B){
        if(B == null) return true;//B树遍历完的时候,且从未出现节点值不相等的情况,就证明是相同的结构
        if(A == null && B != null) return false; 
        if(A.val != B.val) return false;//这个出口条件是反着定义的,因为如果判断true,需要多次比较都相同,是一个且的逻辑,不太好写
        return isSubTreeSame(A.left,B.left) && isSubTreeSame(A.right,B.right);
    }
}

解答2:正向思维

  1. class Solution { 
  2. boolean isSubTreeSame; 
  3. public boolean isSubStructure(TreeNode A, TreeNode B) { 
  4. if(A == null || B == null) return false; 
  5. if(A.val == B.val){//在A中寻找B的根节点,找到后就继续判断此根节点的子树是否相同 
  6. isSubTreeSame = true;//如果使用正的出口条件,需要在每次进入比较之前就对这个变量进行重置 
  7. if(isSubTreeSame(A,B)) return true; 
  8. } 
  9. return isSubStructure(A.left,B) || isSubStructure(A.right,B); 
  10. } 
  11. //输入两个根节点,判断子树结构是否相同 
  12. public boolean isSubTreeSame(TreeNode A,TreeNode B){ 
  13. if(B == null) return isSubTreeSame; 
  14. if(A == null) return false; 
  15. isSubTreeSame = (isSubTreeSame && (A.val == B.val)); 
  16. return isSubTreeSame(A.left,B.left) && isSubTreeSame(A.right,B.right); 
  17. } 
  18. } 

注意: 由于引入了额外的变量isSubTreeSame,需要在进入函数isSubTreeSame()之前,对变量进行重置(第6行),否则会影响到下一次调用的结果。

一种错误写法

2021/3/21,复习时写出一种错误的写法,感觉非常微妙,非常容易被坑。

class Solution {
    public boolean isSubStructure(TreeNode A, TreeNode B) {
        if(A == null || B == null) return false;
        if(A.val == B.val)
            //注意下面两句代码的区别
            //(正确)这句是说:如果条件为true,就返回true,如果条件为false,就什么也不做
            //if(checkSame(A, B)) return true;
            //(错误)这句是说:如果条件为true,就返回true,如果条件为false,就返回false
             return checkSame(A, B);
        return isSubStructure(A.left, B) || isSubStructure(A.right, B);
    }

    private boolean checkSame(TreeNode A, TreeNode B){
        if(B == null) return true;
        if(A == null) return false;
        if(A.val != B.val) return false;
        return checkSame(A.left, B.left) && checkSame(A.right, B.right);
    }
}

详见注释,为什么不能直接返回checkSame(A,B)的结果呢?
这一题是一种嵌套的递归问题,要得到isSubStructure()的结果,是依赖于checkSame()的结果的,当我们遇到A.val == B.val,那么就可以继续调用嵌套在其中的checkSame()做进一步的检查。
关键在于:

  • 这时checkSame()结果如果是true,那么就可以直接返回true,因为已经找到了相同的子结构
  • 但是这时checkSame()结果如果是false,并不能直接返回false,因为这一次没有找到相同的子结构,并不代表别的位置找不到,如果直接返回false,就会导致整个递归函数结束,而没有去继续检查其他位置的情况。
//22.3.28又写错一次
class Solution {
    public boolean isSubStructure(TreeNode A, TreeNode B) {
        if(A == null || B == null) return false;
        if(A.val == B.val){
            if(recur(A, B)) return true;//不可以写成return recur(A,B)
            //因为这个节点不是子结构,还有其他的节点,不能直接返回false
        };
        return isSubStructure(A.left, B) || isSubStructure(A.right, B);//不可以写成recur(A.left,B)||recur(A.right,B)
        //如果直接使用recur(A.left,B)||recur(A.right,B),问题在于:没有找到A子树中与B.val相同的节点,那么还是会造成误判,因为recur的前提是输入的两个节点就是值相等的情况
    }


    private boolean recur(TreeNode A, TreeNode B){//先找到A中值与B.val相同的节点,再调用recur判断B是否是A的子结构
        if(B == null) return true;
        if(A == null && B != null) return false;
        if(A.val != B.val) return false;
        return recur(A.left, B.left) && recur(A.right, B.right);
    }
}
posted @ 2020-07-01 17:37  Howfar's  阅读(258)  评论(0编辑  收藏  举报