「剑指 Offer 26.树的子结构」
题目描述(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。