「剑指 Offer 26.树的子结构」

160 阅读4分钟

「剑指 Offer 26.树的子结构」

Offer 驾到,掘友接招!我正在参与2022春招打卡活动,点击查看活动详情

题目描述(level 中等)

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

示例
  • 示例1
给定的树 A:
     3
    / \
   4   5
  / \
 1   2
       
给定的树 B:
   4 
  /
 1
返回 true,因为 B 与 A 的一个子树拥有相同的结构和节点值。
  • 示例2
输入:A = [1,2,3], B = [3,1]
输出:false
  • 示例3
输入:A = [3,4,5,1,2], B = [4,1]
输出:true
思路分析

终于到了树相关的算法题了,二叉树(Binary Tree)对于二叉树的遍历主要包含四种方式,层序遍历前序遍历中序遍历后序遍历。而层序遍历就是通常所说的广度优先搜索BFS,在剑指Offer 32中使用的就是广度优先搜索算法,一般会借助队列的先进先出特性,一层一层对树进行遍历。前、中、后序遍历则属于深度优先搜索,也就是 Depth FIrst Search 简称DFS,而深度优先搜索的处理方式一般又可以分为递归非递归的方式,非递归的方式一般采用区别于广度优先使用的队列。对于前、中、后序的划分其实是对访问节点的顺序来区分的:

前序遍历:根节点-->左子树-->右子树

中序遍历:左子树-->根节点-->右子树

后序遍历:左子树-->右子树-->根节点

给定树的结构如下:

public class TreeNode {
  int val;
  TreeNode left;
  TreeNode right;
  TreeNode(int x) {
    val = x;
  }
}
  • 前序遍历

根据前序遍历的顺序可以写出递归的代码:

public void preOrderTraverse (TreeNode root) {
  if (null == root) {
    return;
  }
  //仅打印当前节点的值value
  System.out.println(root.val);
  //遍历左子树
  preOrderTraverse(root.left);
  //遍历右子树
  preOrderTraverse(root.right);
}

对于递归方法主要包括递推回溯两个过程,具体的看这里。区别于层序遍历使用队列,深度优先搜索需要借助先进后出特性,非递归实现(迭代法借助栈):

public void preOrderTraverse(TreeNode root) {
  if (null == root) {
    return;
  }
  Deque<TreeNode> stack = new LinkedList<>();
  TreeNode node = root;
  while (!stack.isEmpty || null != node) {
    //遍历左子树
    while (null != node) {
      System.out.println(node.val);
      stack.push(node);
      node = node.left;
    } 
    node = stack.pop();
    node = node.right;
  }
}

这里借助按照前序遍历的顺序遍历二叉树,其实就是 来模拟 递归 中维护的栈,只不过使用迭代的方式显示的将 递归 中计算机维护的栈给表示出来了。原理还是一样的,外层循环使用的是 || 而不是 &&。如果不画图,看起来不够直观,以题目中示例为例,将遍历的结果保存在List集合当中[3,4,1,2,5]:

     3
    / \
   4   5
  / \
 1   2

首先3入栈,执行node = node.left,此时node节点对应的值是4,不为空,继续走内部的while循环,取4的左节点1循环,而1没左节点,此时跳出内部while循环;而stack的存储的顺序为[3,4,1],栈顶元素为1。执行pop操作,node = stack.pop() node = node.right.而1是没有右子节点的,所以此时node = null但是stack不为空,这就相当于递归的回溯阶段了,接着会弹出节点4而其是有右节点的,node != null进入内部while循环,此时集合的结果为[3,4,1,2],依次类推直到整个树被遍历完成。个人更喜欢这种迭代的方式来模拟递归操作。当然还有另一种实现方式:

public  void preOrderIteration(TreeNode root) {
	if (null == root) {
		return;
	}
	Deque<TreeNode> stack = new LinkedList<>();
	stack.push(root);
	while (!stack.isEmpty()) {
		TreeNode node = stack.pop();
		System.out.println(node.val);
		if (node.right != null) {
			stack.push(node.right);
		}
		if (node.left != null) {
			stack.push(node.left);
		}
	}
}
//前序遍历顺序是:根节点-->左子树-->右子树,栈作为先进后出的特性,所以这里先添加是“右节点-->左节点”
  • 中序遍历

遍历方向为:左子树-->根节点-->右子树,递归实现如下:

public void inorderTraversal(TreeNode node) {
  if(null == node) {
    return;
  }
  inorderTraversal(node.left);
  //对节点的操作在这里,可以打印,添加到集合list中...
  System.out.println(node.val);
  inorderTraversal(node.right);
}

还是借助来模拟递归的操作:

public void inorderTraversal(TreeNode root) {
  if (null == root) {
    return;
  }
  Deque<TreeNode> stack = new LinkedList<>();
  TreeNode node = root;
  while (!stack.isEmpty() || null != node) {
    while (null != node) {
      stack.push(node);
      node = node.left;
    }
    node = stack.pop();
    System.out.println(node.val);
    node = node.right;
  }
}
  • 后序遍历

遍历方向为左子树-->右子树-->根节点,递归实现如下:

public void postorderTraversal(TreeNode root) {
  if(null == root) {
    return;
  }
  postorderTraversal(root.left);
  postorderTraversal(root.right);
  //对遍历的节点进行操作
  System.out.println(node.val);
}

迭代法:

public void postorderTraversal2(TreeNode root) {
  if (root == null) {
    return;
  }
  Deque<TreeNode> stack = new LinkedList<>();
  TreeNode prev = null;
     
  while (!stack.isEmpty() || root != null) {
    while (root != null) {
      stack.push(root);
      root = root.left;
    }
    root = stack.pop();
    if (root.right == null || root.right == prev) {
      System.out.println(root.val);
      prev = root;
      root = null;
    } else {
      stack.push(root);
      root = root.right;
    }
  }
}

其实无论是前序、中序、还是后序的迭代法,中心思想都是一样的,借助来还原递归时计算机内部维护的栈结构,分析的方法与前序的迭代法相差不大,画图可能更加好理解一点。

  • 层序遍历

层序遍历的使用

  • Morris 遍历

这个有时间在补上😭,卷不动了。

回到正题

判断B是否是A的子结构,需要分三种情况讨论,B可能是A的左子树,右子树、或者以A中某一节点为根节点的子树。对于前两种情况,其实就是此时B已经退化为链表结构了。对于第三种情况,就需要比较A、B对应节点是否能完全匹配上。

A树
     3
    / \
   4   5
  / \
 1   2
 
B树(一)
  4
 /
1

B树(二)
  3
   \
    5
  
B树(三)
     3
    / \
   4   5
//以上三种情况对应了三种分析的情况
代码实现
public Solution {
   public boolean isSubStructure(TreeNode A, TreeNode B) {
     return (A != null && B != null)
       && (compare(A, B) 
           || isSubStructure(A.left, B) 
           || isSubStructure(A.right, B));
    }

    boolean compare(TreeNode A, TreeNode B) {
        if (null == B) {
            return true;
        }
        if (null == A || A.val != B.val) {
            return false;
        }
        return compare(A.left, B.left) && compare(A.right, B.right);
    }
}
复杂度

时间复杂度O(MN):M、N分别对应A、B树的节点的数量。

空间复杂度O(M):都退化为链表结构是,总的递归深度为A树的节点个数M

链接

树的子结构