二叉树的层次遍历经典问题
一、层次遍历简介
广度优先遍历又称层次遍历,过程如下:
层次遍历就是从根节点开始,先访问根节点下面一层全部元素,再访问之后的层次,图里就是从左到右一层一层的去遍历二叉树,先访问3,之后访问的左右子孩子9和20,之后分别访问9和20的左右子孩子[8,13]和[15,17],最后得到结果[3,9,20,8,13,15,17]。
这里的问题是怎么将遍历过的元素的子孩子保存一下呢,例如访问9时其左右子孩子8和13应该先存一下,直到处理20之后才会处理。使用队列来存储能完美解决上述问题,例如上面的图中:
思考:该过程不复杂,如果能将树的每层次分开了,是否可以整点新花样?首先,能否将每层的元素顺序给反转一下呢?能否奇数行不变,只将偶数行反转呢?能否将输出层次从低到root逐层输出呢?再来,既然能拿到每一层的元素了,能否找到当前层最大的元素?最小的元素?最右的元素(右视图)?最左的元素(左视图)?整个层的平均值?
很明显都可以!这么折腾有啥用呢?没啥用!但这就是层次遍历的高频算法题!这就是LeetCode里经典的层次遍历题!
102.二叉树的层序遍历
107.二叉树的层次遍历II
199.二叉树的右视图
637.二叉树的层平均值
429.N叉树的前序遍历
515.在每个树行中找最大值
116.填充每个节点的下一个右侧节点指针
117.填充每个节点的下一个右侧节点指针l
103锯齿层序遍历
除此之外,在深度优先的题目里,有些仍然会考虑层次遍历的实现方法。
二、基本的层序遍历与变换
2.1 层序遍历基础版
最简单的情况:仅仅层序遍历并输出所有元素
利用前面讲到的借助队列的方法来实现
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;
}
2.2 二叉树的层序遍历(模版:非常重要)
![在这里插入图片描述](https://img-blog.csdnimg.cn/direct/c23f7ece838341c491b3918718f405e5.pn
LeetCode102:给你一个二叉树,请你返回其按层序遍历得到的节点值。(即逐层地,从左到右访问所有节点)。
注意:与前面相比,要区分每一层
如何判断某一层遍历完了?可以设定一个size变量来标记。size表示某一层的元素个数,只要元素出队,size-1.当size==0时,该层已经遍历完了。(代码里的做法与这里殊途同归)
然后此时队里的元素个数为下一层元素的个数,因此让size标记为下一层的元素个数即可处理下一层了。
最后,把每一层遍历到的结点放入一个结果集中,将其返回即可
class Solution {public List<List<Integer>> levelOrder(TreeNode root) {if(root == null) return new ArrayList<List<Integer>>();//创建集合和队列List<List<Integer>> list = new ArrayList<>();LinkedList<TreeNode> queue = new LinkedList();//将根结点放入队列,方便进入循环queue.add(root);while(queue.size() > 0){//everylist集合用来记录某一层的所有元素List<Integer> everylist = new ArrayList<>();//size记录某一层的元素个数int size = queue.size();//从队头遍历某一层的所有元素,并把下一层的元素存放到队尾for(int i = 0; i < size; i++){TreeNode t = queue.remove();everylist.add(t.val);if(t.left != null)queue.add(t.left);if(t.right != null)queue.add(t.right);}//此时i == size,该层所有元素都遍历完了list.add(everylist);}return list;}
}
2.3 层序遍历-自底往上
LeetCode107.给定一个二叉树,返回其节点值自底向上的层序遍历。(即按从叶子节点所在层到根节点所在的层,逐层从左向右遍历)。例如给定的二叉树与结果为下图:
实现的代码几乎与正常的层序遍历相同,只在list.add(0,everylist)这里把后面遍历的层次放到list的前面,实现自底往上的效果。
为了降低在结果列表的头部添加一层节点值的列表的时间复杂度,结果列表可以使用链表的结构,即创建list时把ArrayList改成LinkedList。在链表头部添加一层节点值的列表的时间复杂度是O(1)。
class Solution {public List<List<Integer>> levelOrderBottom(TreeNode root) {if(root == null) return new LinkedList<List<Integer>>();List<List<Integer>> list = new LinkedList<>();LinkedList<TreeNode> queue = new LinkedList();queue.add(root);while(queue.size() > 0){List<Integer> everylist = new ArrayList<>();int size = queue.size();for(int i = 0; i < size; i++){TreeNode t = queue.remove();everylist.add(t.val);if(t.left != null)queue.add(t.left);if(t.right != null)queue.add(t.right);}list.add(0,everylist);}return list;}
}
2.4 二叉树的锯齿形层次遍历
LeetCode103题,要求是:给定一个二叉树,返回其节点值的锯齿形层序遍历。(即先从左往右,再从右往左进行下一层遍历,以此类推,层与层之间交替进行)。
例如:给定二叉树[3,9,20,nul,null,15,7]
依旧是对2.2对代码进行改写。因为这里有时是从左到右,有时是从右到左,自然可以想到把代码里的队列改成双端队列,然后再设置过参数判断元素存放位置。
如果从左至右,我们每次将被遍历到的当前层元素插入至双端队列的末尾。如果从右至左,我们每次将被遍历到的元素插入至双端队列的头部。
class Solution {public List<List<Integer>> zigzagLevelOrder(TreeNode root) {if(root == null) return new ArrayList<List<Integer>>();List<List<Integer>> list = new LinkedList<>();Queue<TreeNode> queue = new LinkedList();queue.add(root);Boolean judgeleft = true;while(queue.size() > 0){//创建成双端队列Deque<Integer> everylist = new LinkedList<>();int size = queue.size();for(int i = 0; i < size; i++){TreeNode t = queue.remove();//从左到右放队尾if(judgeleft) everylist.offerLast(t.val);//从右到左放队首else everylist.offerFirst(t.val);if(t.left != null)queue.add(t.left);if(t.right != null)queue.add(t.right);}judgeleft = !judgeleft;//把双端队列转类型list.add(new LinkedList<Integer>(everylist));}return list;}
}
2.5 N叉树的层序遍历
LeetCode.429给定一个N叉树,返回其节点值的层序遍历。(即从左到右,逐层遍历)树的序列化输入是用层序遍历,每组子节点都由null值分隔(参见示例)
N叉树的定义如下,就是一个值,加一个列表,其类型仍然是Node:
这个也是102的扩展,很简单的广度优先,与二叉树的层序遍历基本一样,借助队列即可实现。只有在保存当前结点的孩子时,不是左右孩子,有N个,可以使用增强for循环来遍历
class Solution {public List<List<Integer>> levelOrder(Node root) {if(root == null)return new ArrayList<List<Integer>>();List<List<Integer>> list = new ArrayList();Queue<Node> queue = new LinkedList();queue.add(root);while(queue.size() > 0){List<Integer> everylist = new ArrayList();int size = queue.size();for(int i = 0; i < size; i++){Node t = queue.remove();everylist.add(t.val);//与2.2唯一不同之处,利用增强for循环遍历所有孩子for(Node node1: t.children) {if(node1 != null) queue.add(node1);}}list.add(everylist);}return list;}
}
三、几个处理每层元素的题目
如果我们拿到了每一层的元素,那是不是可以利用一下造几个题呢?例如每层找最大值、平均值、最右侧的值呢?当然可以。LeetCode.里就有三道非常明显的题目。
515.在每个树行中找最大值
637.二叉树的层平均值
199.二叉树的右视图
3.1 在每个树行中找最大值
LeetCode515题目要求:给定一棵二叉树的根节点root,请找出该二叉树中每一层的最大值。
与模版相比,在while循环中不需要创建另外一个集合,只需要用max记录最大值,然后添加进list集合即可
class Solution {public List<Integer> largestValues(TreeNode root) {if(root == null) return new ArrayList<Integer>();List<Integer> maxlist = new ArrayList();Queue<TreeNode> queue = new LinkedList();queue.add(root);while(queue.size() > 0){int max = Integer.MIN_VALUE;int size = queue.size();for(int i = 0; i < size; i++){TreeNode t =queue.remove();max = Math.max(max,t.val);if(t.left != null) queue.add(t.left);if(t.right != null) queue.add(t.right);}maxlist.add(max);}return maxlist;}
}
3.2 在每个树行中找平均值
LeetCode637要求给定一个非空二叉树,返回一个由每层节点平均值组成的数组
这些题目其实都差不多一个思路,只要中间一些处理修改一下就行。这道题就把每层元素的和记录下来,除以元素个数就行
class Solution {public List<Double> averageOfLevels(TreeNode root) {List<Double> list = new ArrayList();if(root == null) return list;Queue<TreeNode> queue = new LinkedList();queue.add(root);while(queue.size() > 0){int size = queue.size();double sum = 0;for(int i = 0; i < size; i++){TreeNode t = queue.remove();sum += t.val;if(t.left != null) queue.add(t.left);if(t.right != null) queue.add(t.right);}list.add(sum/size);}return list;}
}
3.3 二叉树的右视图
LeetCode199题目要求是:给定一个二叉树的根节点root,想象自己站在它的右侧,按照从顶部到底部的顺序,返回从右侧所能看到的节点值。例如:
这道题只要把每层最后一个元素保存起来即可
class Solution {public List<Integer> rightSideView(TreeNode root) {List<Integer> list = new ArrayList<>();if(root == null) return list;Queue<TreeNode> queue = new LinkedList();queue.add(root);while(queue.size() > 0){int size = queue.size();for(int i = 0; i < size; i++){TreeNode t = queue.remove();if(size - 1 == i) list.add(t.val);if(t.left != null) queue.add(t.left);if(t.right != null) queue.add(t.right);}}return list;}
}
3.4 最底层最左边
上面这个层次遍历的思想可以方便的解决LeetCode513.二叉树最底层最左边的值的问题:给定一个二叉树的根节点root,请找出该二叉树的最底层最左边节点的值。假设二叉树至少有一个节点。
如果是正常遍历,是从左到右,那最后会遍历到最底层最右边,与题目正好相反。所以,我们在遍历存储下一层元素的时候,可以先保存右边的节点,再保存左边的节点。这样,遍历就是从右到左,题目就解出来啦。
class Solution {public int findBottomLeftValue(TreeNode root) {//可以记录值;也可记录节点,最后返回值int num = 0;Queue<TreeNode> queue = new LinkedList();queue.add(root);while(queue.size() > 0){int size = queue.size();for(int i = 0; i < size; i++){TreeNode t = queue.remove();num = t.val;if(t.right != null) queue.add(t.right);if(t.left != null) queue.add(t.left);}}return num;}
}