序言
在LeetCode刷二叉树题目时,看到有人专门整理了有关于层序遍历的问题,所以自己来实现一遍,做个笔记。
前排提醒
如果笔记有错,请各位批评指正;如果觉得笔记垃圾,那就只看看题目的收录吧,或许可以节省你找题的时间。
层序遍历的题目收录
零、层序遍历模板
- 层序遍历思路
解决层序遍历的问题还得靠队列来实现,队列的先进先出特点正符合我们的按层遍历的要求,这其实也是BFS的一种思想。开始时,我们将根节点入队列,根节点出队列时将他的左右子节点入队列,这样我们就得到了第二层的结点;依次操作,我们就可以得到 层序遍历的结果。
- 代码实现
//层序遍历模板
class Solution {
public void levelOrder(TreeNode root) {
Queue<TreeNode> queue = new ArrayDeque<>();
if(root != null){
queue.add(root);
}
while(!queue.isEmpty()){
TreeNode node = queue.poll();
if(node.left != null){
queue.add(node.left);
}
if(node.right != null){
queue.add(node.right);
}
}
}
}
- 算法总结
层序遍历的模板代码其实也是BFS代码的一部分,只要我们融会贯通,善于运用队列,相信我们能够战胜一大波题目。
一、102.二叉树的层序遍历
- 题目描述
给你一个二叉树,请你返回其按 层序遍历 得到的节点值。 (即逐层地,从左到右访问所有节点)。
- 解决思路
按照题目要求,我们需要返回一个二维数组,也即要把每一层 都分开,这时候我们只需要对模板程序稍作修改即可: 我们在进行层序遍历时,可以观察到当上一层的结点从队列出来时,刚进去的是下一层结点,而当上层结点都出去时,下层结点也都刚刚进去。所以,只要我们实时返回一个队列大小,就可以解决按层输出的问题。
- 代码实现
//按层返回的层序遍历
class Solution {
public List<List<Integer>> levelOrder(TreeNode root) {
Queue<TreeNode> queue = new ArrayDeque<>();
List<List<Integer>> list = new ArrayList<>();
if(root != null){
queue.add(root);
}
while(!queue.isEmpty()){
int n = queue.size();
List<Integer> l = new ArrayList<>();
for(int i =0 ; i < n; i++){
TreeNode node = queue.poll();
l.add(node.val);
if(node.left != null){
queue.add(node.left);
}
if(node.right != null){
queue.add(node.right);
}
}
list.add(l);
}
return list;
}
}
- 结果分析
每个结点进队列一次,时间复杂度为O(N)。
- 算法总结
上面代码部分我给出了层序遍历的模板,单看层序遍历还是很简单的,只是一个队列的运用。若要符合题目要求,再稍作改进即可。我们平时刷题何尝不是这样,一套模板就可以打败还几道题,这就要求我们做到善于总结,举一反三。
二、P103 二叉树的锯齿形层序遍历
- 题目描述
给定一个二叉树,返回其节点值的锯齿形层序遍历。(即先从左往右,再从右往左进行下一层遍历,以此类推,层与层之间交替进行)。
- 解决思路
层次遍历+标记
首先实现二叉树的层序遍历,我们在层序遍历的基础上稍作修改,实现锯齿形层序遍历。
若根节点为第1层,可以观察到,奇数层结点从左到右输出,偶数层倒序输出即可。我们加一个变量level来标记奇数层或偶数层。
单独设置一个双端队列来记录每一层的元素。当为奇数层时,因为是从左到右输出,而且层序遍历也是从左到右遍历,所以加到双端队列的尾部即可;当为偶数层时,需要从右往左输出,与层序遍历逆序,所以加到双端队列头部以保证逆序。
- 代码实现
class Solution {
public List<List<Integer>> zigzagLevelOrder(TreeNode root) {
List<List<Integer>> list = new ArrayList<>();
Queue<TreeNode> queue = new ArrayDeque<>();
if(root != null){
queue.add(root);
}
int level = 0;
while(!queue.isEmpty()){
level += 1;
Deque<Integer> l = new LinkedList<Integer>();
int n = queue.size();
for(int i = 0; i < n ; i++){
//始终在队列头部取结点
TreeNode node = queue.poll();
if(level % 2 == 0){
//偶数层,从右到左输出,加到双端队列头部
l.addFirst(node.val);
}else{
//奇数层,从左向右输出,加到双端队列尾部
l.addLast(node.val);
}
if(node.left != null)
queue.add(node.left);
if(node.right != null)
queue.add(node.right);
}
list.add(new LinkedList<>(l));//需要进行类型转换
}
return list;
}
}
- 结果分析
每个结点遍历一次,空间复杂度为O(n)。
- 算法总结
该题目其实就是考察我们对层序遍历的理解,基于此,再考察我们数据结构用的怎么样。所以说,练好数据结构,算法刷题就又多了一个工具。
要求我们熟练掌握队列等数据结构。Java中的ArrayDeque性能较好,功能较多,可模拟栈、队列、双端队列,推荐大家使用。
三、P104 二叉树的最大深度(BFS)
- 题目描述
给定一个二叉树,找出其最大深度。
二叉树的深度为根节点到最远叶子节点的最长路径上的节点数。
说明: 叶子节点是指没有子节点的节点。
- 解决思路
广度优先搜索实现。我们换一种思维,求解二叉树的最大深度,换句话说,就是求二叉树的层数,对不对?还记得我们上面两道题是有关于层序遍历呢吗?当题目要求我们以二维数组的形式按层返回二叉树的结点时, 我们对BFS稍作修改,每次从队列中拿出当前层的所有结点来拓展,那么现在,我们只要再设置一个变量记录层数不就解决此题了吗?
- 代码实现
//BFS
class Solution {
public int maxDepth(TreeNode root) {
if(root == null){
return 0;
}
Queue<TreeNode> queue = new ArrayDeque<>();
queue.add(root);
int count = 0;//记录层数
while(!queue.isEmpty()){
int n = queue.size();
while(n > 0){
TreeNode node = queue.poll();
if(node.left != null){
queue.add(node.left);
}
if(node.right != null){
queue.add(node.right);
}
n --;
}
count ++;
}
return count;
}
}
- 结果分析
在时间上来说,需要遍历完所有的节点,所以时间复杂度为O(n)。空间上,BFS的方法则取决于队列的大小。
- 算法总结
一连做了三道题目,对于二叉树的广度优先搜索有了初步的理解,希望自己在以后碰到类似题目时也可以独立解决。
四、107.二叉树的层次遍历II
- 题目描述
给定一个二叉树,返回其节点值自底向上的层序遍历。 (即按从叶子节点所在层到根节点所在的层,逐层从左向右遍历)
- 解决思路
这道题目与P102二叉树层序遍历I的区别就在于这是倒序输出每一层结点。最简单的方法就是在之前的基础上加一个辅助栈。还有一种方法就是将list改装一下,每次添加元素时,加到它的首部,就能够实现倒序的输出。
- 代码实现
//方法一
class Solution {
public List<List<Integer>> levelOrderBottom(TreeNode root) {
List<List<Integer>> list = new ArrayList<>();
Stack<List<Integer>> stack = new Stack<>();
Queue<TreeNode> queue = new ArrayDeque<>();
if(root != null){
queue.add(root);
}
while(!queue.isEmpty()){
List<Integer> l = new ArrayList<>();
int n = queue.size();
while(n > 0){
TreeNode node = queue.poll();
l.add(node.val);
if(node.left != null){
queue.add(node.left);
}
if(node.right != null){
queue.add(node.right);
}
n --;
}
stack.add(l);
}
while(!stack.isEmpty()){
list.add(stack.pop());
}
return list;
}
}
class Solution {
public List<List<Integer>> levelOrderBottom(TreeNode root) {
List<List<Integer>> list = new LinkedList<>();
Queue<TreeNode> queue = new ArrayDeque<>();
if(root != null){
queue.add(root);
}
while(!queue.isEmpty()){
List<Integer> l = new ArrayList<>();
int n = queue.size();
while(n > 0){
TreeNode node = queue.poll();
l.add(node.val);
if(node.left != null){
queue.add(node.left);
}
if(node.right != null){
queue.add(node.right);
}
n --;
}
list.add(0, l);
//public void add(int index,E element):将元素添加到指定索引位置
}
return list;
}
}
- 结果分析
时间复杂度是O(n)。
- 算法总结
两种方法的时间和空间消耗差不多,后者略好一点。相较于之前做的P102,只需做一点小小的修改即可。所以说,多刷点题,没坏处,哦,坏处可能就是会秃。
五、637.二叉树的层平均值
- 题目描述
给定一个非空二叉树, 返回一个由每层节点平均值组成的数组。
- 解决思路
按层进行层序遍历,记录每一层的数量和节点值总和,计算平均值并返回。
- 代码实现
class Solution {
public List<Double> averageOfLevels(TreeNode root) {
List<Double> list = new ArrayList<>();
Queue<TreeNode> queue = new ArrayDeque<>();
if(root != null) {
queue.add(root);
}
while(!queue.isEmpty()) {
double sum = 0; //每一层结点数值的总和
int n = queue.size();
int n1 = n;//每一层的结点数
while(n > 0) {
TreeNode node = queue.poll();
sum += node.val;
if(node.left != null) {
queue.add(node.left);
}
if(node.right != null) {
queue.add(node.right);
}
n --;
}
Double d = sum / n1;
list.add(d);
}
return list;
}
}
- 结果分析
时间复杂度为O(N),空间复杂度为O(N),N为二叉树节点的个数。
- 算法总结
经过几个层序遍历题目的熏陶,这种简单类型的题目应该不成问题。
六、429.N叉树的前序遍历
- 题目描述
给定一个 N 叉树,返回其节点值的层序遍历。(即从左到右,逐层遍历)。树的序列化输入是用层序遍历,每组子节点都由 null 值分隔(参见示例)。就是说,每一个节点的子节点序列是由null值分隔的。
- 解决思路
运用层序遍历模板,只是向队列中加入子节点时不只是加入左右子树了,而是遍历加入所有的子节点。
- 代码实现
class Solution {
public List<List<Integer>> levelOrder(Node root) {
List<List<Integer>> res = new ArrayList<>();
Queue<Node> queue = new ArrayDeque<>();
if (root == null) {
return new ArrayList<>();
}
queue.add(root);
while (!queue.isEmpty()) {
int n = queue.size();
List<Integer> list = new ArrayList<>();
while (n > 0) {
Node node = queue.poll();
list.add(node.val);
for (Node i : node.children) {
queue.add(i);
}
n--;
}
res.add(list);
}
return res;
}
}
-
结果分析
-
算法总结
层序遍历模板稍作修改。
七、515.在每个树行中找最大值
- 题目描述
给定一棵二叉树的根节点 root ,请找出该二叉树中每一层的最大值。
- 解决思路
按层进行层序遍历,然后逐个比较元素,记录每一层的最大值。
- 代码实现
class Solution {
public List<Integer> largestValues(TreeNode root) {
if(root == null){
return new ArrayList<Integer>();
}
List<Integer> res = new ArrayList<>();
ArrayDeque<TreeNode> tree = new ArrayDeque<>();
tree.add(root);
int maxi = root.val;
while(!tree.isEmpty()){
int n = tree.size();
maxi = tree.peek().val;
while(n > 0){
TreeNode node = tree.poll();
if(node.val > maxi){
maxi = node.val;
}
if(node.left != null){
tree.add(node.left);
}
if(node.right != null){
tree.add(node.right);
}
n--;
}
res.add(maxi);
}
return res;
}
}
- 结果分析
O(N),O(N)
- 算法总结
注意是输出每一层的最大值,所以对每一层进行遍历时,假设每层的第一个节点为max,进而继续比较,更新max。
八、116.填充每个节点的下一个右侧节点指针
- 题目描述
给定一个 完美二叉树 ,其所有叶子节点都在同一层,每个父节点都有两个子节点。二叉树定义如下:
struct Node {
int val;
Node *left;
Node *right;
Node *next;
}
填充它的每个 next 指针,让这个指针指向其下一个右侧节点。如果找不到下一个右侧节点,则将 next 指针设置为 NULL。
初始状态下,所有 next 指针都被设置为 NULL。
进阶:
你只能使用常量级额外空间。也可以使用递归。
- 解决思路
可以用层序遍历解决,但是不是常量级空间。按照层序遍历的思想,对每一层进队列的结点,如果是本层最后一个结点就指向NULL,否则,指向下一个进队列的节点。
- 代码实现
class Solution {
public Node connect(Node root) {
Deque<Node> queue = new ArrayDeque<>();
if(root != null){
queue.add(root);
}
while(!queue.isEmpty()){
int n = queue.size();
while(n > 0){
Node node = queue.poll();
//判断是不是本层最后一个节点
if(n != 1){
//n==1说明到了本层最后一个元素,最后一个元素的next指针指向NULL,
//而初始时就已经指向NULL了,不用我们去人为的复制NLL。
node.next = queue.element();
//element 只返回不移出
}
if(node.left != null){
queue.add(node.left);
}
if(node.right != null){
queue.add(node.right);
}
n --;
}
}
return root;
}
}
//方法二
class Solution {
public Node connect(Node root) {
if(root == null){
return root;
}
Node leftmost = root;//最左节点
//如果最左节点的左子树为空,那么说明到了最后一层了
while(leftmost.left != null){
Node head = leftmost;
//head就是当前层的各个节点,为空说明当前层完成了遍历
while(head != null){
//第一种连接:根节点的左右子树
head.left.next = head.right;
//第二种连接:不同根节点之间的连接
if(head.next != null){
//当前节点在该层有next节点
head.right.next = head.next.left;
}
//继续本层的下一个节点
head = head.next;
}
//当前层完成后,继续下一层
//去下一层的最左节点
leftmost = leftmost.left;
}
return root;
}
}
//方法三
class Solution {
public Node connect(Node root) {
if(root == null){
return root;
}
if(root.left != null){
root.left.next = root.right;
if(root.next != null){
root.right.next = root.next.left;
}
connect(root.left);
connect(root.right);
}
return root;
}
}
- 结果分析
时间空间复杂度都为O(N)。
- 算法总结
层序遍历不是最优。方法二和方法三本质上也是一种层序遍历,仍然是采用层序遍历的思想一层一层的遍历,只是形式上不是层序遍历的模板而已。
九、117.填充每个节点的下一个右侧节点指针II
- 题目描述
填充每个节点的下一个右侧节点指针 II
- 解决思路
*此题与上题的不同之处在于此题不是满二叉树。这就改变了next指针的连接类型。
(1)空间复杂度O(n).仍采用层次遍历的方法。
(2)空间复杂度O(1).此时不再是两种类型的连接,而是多种类型的next连接。
这就失去了头绪,如果最左节点为空,那么我们也找不到它。解决办法就是在最左方加一个哑节点,用来串联next指针。
思路与上题一样,遍历本层的同时,创建下一层next指针,然后再按此方法逐层遍历。(将每一层想像成一个链表)
- 代码实现
class Solution {
public Node connect(Node root) {
Deque<Node> q = new ArrayDeque<>();
if(root != null){
q.add(root);
}
while(!q.isEmpty()){
int n = q.size();
while(n > 0){
Node node = q.poll();
if(n != 1){
node.next = q.element();
}
if(node.left != null){
q.add(node.left);
}
if(node.right != null){
q.add(node.right);
}
n --;
}
}
return root;
}
}
//方法二
class Solution {
public Node connect(Node root) {
if(root == null){
return root;
}
//cur负责第 N 层的遍历
Node cur = root;
while(cur != null){
//哑节点在N + 1层
Node dummy = new Node(0);
//pre负责第 N + 1 层的遍历
Node pre = dummy;
while(cur != null){
if(cur.left != null){
pre.next = cur.left;
pre = pre.next;
}
if(cur.right != null){
pre.next = cur.right;
pre = pre.next;
}
//N层的节点遍历
cur = cur.next;
}
//去下一层,dummy.next为下一层的除哑节点外的第一个节点
cur = dummy.next;
}
return root;
}
}
- 结果分析
方法二空间复杂度O(1),是在原树上做的遍历修改。
- 算法总结
方法二采用哑节点进行遍历时每一层的最后一个元素其实不用人为的去让他指向NULL,因为最初始的时候每一个节点的next指针就是指向NULL的。上一个题也是这样。
十、199.二叉树的右视图
- 题目描述
给定一个二叉树的 根节点 root,想象自己站在它的右侧,按照从顶部到底部的顺序,返回从右侧所能看到的节点值。
- 解决思路
按照层序遍历的套路,只要依次记录每一层的最后一个元素就可以了。
- 代码实现
class Solution {
public List<Integer> rightSideView(TreeNode root) {
if(root == null){
return new ArrayList<>();
}
List<Integer> res = new ArrayList<>();
Deque<TreeNode> tree = new ArrayDeque<>();
tree.add(root);
while(!tree.isEmpty()){
int n = tree.size();
while(n > 0){
TreeNode node = tree.poll();
if(n == 1){
res.add(node.val);
}
if(node.left != null){
tree.add(node.left);
}
if(node.right != null){
tree.add(node.right);
}
n-- ;
}
}
return res;
}
}
- 结果分析
时间复杂度为O(N),空间复杂度为O(N)。
- 算法总结
层序遍历变形。