1. 树的层次遍历与相关面试题
1.1 层次遍历简介
广度优先在面试里出现的频率非常高,但是相对简单,题目也比较少,常见的题目也就七八道。
广度优先又叫层次遍历,基本过程如下:
层次遍历就是从根节点开始,先访问根节点下面一层全部元素,再访问之后的层次,类似金字塔一样一层层访问。上面的图示按照层次访问的结果就是:[1,2,3,4,5,6,7]
我们可以看到这里就是从左到右一层一层的去遍历二叉树,先访问1,之后访问1的左右子孩子2和3,之后分别访问2和3的左右子孩子[4,5]和[6,7]。
由此我们发现如果使用队列来存储的话,访问某一层的时候就将该层的元素全部入队,某个元素出队的时候,就将该元素的左右子节点分别入队,就能保证完美访问所有元素。例如上面的图中:
- 首先1入队。
- 然后1出队,之后将1的左右子孩子2和3入队。
- 之后2出队,将2的左右子孩子4和5入队。
- 之后3出队,将3的左右子孩子6和7入队。
- 之后4,5,6,7分别出队,因为都没有子孩子了,所以都只出队就行了。
该过程不复杂,我们再来看一下,如果能将层次分开了,那我们能整出什么花样?
首先,我能否将每层的顺序反转一下呢?那这就是一棵树的镜像问题了。那能否奇数行不变,偶数行反转呢?那就是锯齿状输出元素了。我能否将输出层次从低到root逐层输出呢?那这就是层的反转问题了。
再来,既然能拿到每一层的元素了,我能否找到当前层最大的元素?最小的元素?最右的元素(右视图)?最左的元素(左视图)?整个层的平均值?
很明显都可以,但是这么折腾有啥用呢?没啥用,但是如果告诉你这几种情况就是层次遍历的常见算法题,你还觉得没用吗?如果告诉你上述几个折腾就是下面的LeetCode题,你还觉得没用吗?如果告诉你研究清楚如何将层次分开,这些问题就不用做了,你还觉得没用吗?
LeetCode 102.二叉树的层序遍历
LeetCode 107.二叉树的层次遍历II
LeetCode 199.二叉树的右视图
LeetCode 637.二叉树的层平均值
LeetCode 429.N叉树的前序遍历
LeetCode 515.在每个树行中找最大值
LeetCode 116.填充每个节点的下一个右侧节点指针
LeetCode 117.填充每个节点的下一个右侧节点指针II
LeetCode 103.锯齿层序遍历
1.2 基本的层序遍历与变换
我们先看最简单的情况,仅仅遍历并输出一遍,不考虑分层的问题。基本的层次遍历方法如下,这个代码很容易理解,就是先访问根节点,然后将其左右子孩子放到队列里,接着继续出队,出来的元素都将其左右自孩子放到队列里知道队列为空了就退出。
/**
*树结构如下
* 3
/ \
9 20
/ \
15 7
* 应输出结果 [3, 9, 20, 15, 7]
*/
public static List<Integer> simpleLevelOrder(TreeNode root) {
if (root == null) {
return new ArrayList<Integer>();
}
List<Integer> res = new ArrayList<Integer>();
LinkedList<TreeNode> queue = new LinkedList<TreeNode>();
//将根节点放入队列中,然后不断遍历队列
queue.add(root);
//有多少元素执行多少次
while (queue.size() > 0) {
//获取当前队列的长度,这个长度相当于当前这一层的节点个数
TreeNode t = queue.remove();
res.add(t.val);
if (t.left != null) {
queue.add(t.left);
}
if (t.right != null) {
queue.add(t.right);
}
}
return res;
}
根据树的结构可以看到,一个结点在一层访问之后,其子孩子都是在下层按照FIFO的顺序才处理的,因此队列就是一个缓存的作用。
我们继续加码?我们该如何将每层的元素分开呢?请看一下题。
1.2.1 LeetCode102 二叉树的层序遍历
题目要求:给你一个二叉树,请你返回其按层序遍历得到的节点值。(即逐层地,从左到右访问所有节点)。
示例1:
二叉树:[3,9,20,null,null,15,7]
3
/ \
9 20
/ \
15 7
输出:[[3],[9,20],[15,7]]
示例 2:
输入:root = [1]
输出:[[1]]
示例 3:
输入:root = []
输出:[]
广度优先需要用队列作为辅助结构,我们先将根节点放到队列中,然后不断遍历队列。这里的问题是如何判断某一层访问完了呢?很简单,用一个变量size就完了,size表示某一层的元素个数,只要出队,就将size减1,见到0就说明该层元素访问完了。
很容易想到,size变成0之后,就该为其设置下一层的元素个数。那问题又来了,size该怎么知道每层的元素是多少呢?为了更清晰,我们增加几个结点,然后看一下执行的过程图:
从图中可以看出,首先拿出根节点,如果左子树/右子树不为空,就将他们放入队列中。处理完后,根节点已经从队列中拿走了,而根节点的两个孩子已放入队列中了,现在队列中就有两个节点9和20。恰好就是第二层的所有结点。
继续,我们将9从队列中拿走,并将其子孩子8和13入队。之后再将20出队,并将其子孩子15和7入队,这是我们发现当第二层的结点访问完的时候,队列的里的元素恰好都是第三层的元素。
综上,我们可以得到结论:当某一层元素访问完毕(size–一直到0就表示该层访问完),当size变成0时,只要让size重新等于队列元素的个数(size = queue.size())就行了。
最后,我们把每层遍历到的节点都放入到一个结果集中,最后返回这个结果集就可以了。代码如下:
class Solution {
public List<List<Integer>> level102Order(TreeNode root) {
if(root==null) {
return new ArrayList<List<Integer>>();
}
List<List<Integer>> res = new ArrayList<List<Integer>>();
LinkedList<TreeNode> queue = new LinkedList<TreeNode>();
//将根节点放入队列中,然后不断遍历队列
queue.add(root);
while(queue.size()>0) {
//获取当前队列的长度,这个长度相当于当前这一层的节点个数
int size = queue.size();
ArrayList<Integer> tmp = new ArrayList<Integer>();
//将队列中的元素都拿出来(也就是获取这一层的节点),放到临时list中
//如果节点的左/右子树不为空,也放入队列中
for