16.面试算法-树的层次遍历与相关面试题

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
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值