二叉树2. 层次遍历之一

Posted 纵横千里,捭阖四方

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了二叉树2. 层次遍历之一相关的知识,希望对你有一定的参考价值。

树的遍历有两张基本的方式,广度优先和深度优先两种基本的方式。广度优先相对简单,题目也比较少,整个LeetCode也就10来道题目。而深度优先的类型多、变换多,难度差异也很大,所以我们先将层次遍历的9道题吃掉。之后专心对付深度优先。

广度优先又叫层次遍历,层次遍历其基本过程如下所示:

层次遍历就是从根结点开始,先访问根节点下面一层全部元素,再访问之后的层次,类似金字塔一样一层层访问。上面的图示按照层次访问的结果就是:

[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分别出队,因为都没有自孩子了,所以都只出队就行了。

由此在几乎所有与层次访问或者广度优先的场景都少不了队列的应用,这也是我们前面说队列的考察最喜欢和广度优先的场景一起进行。

接下来我们要吃掉9道层次遍历相关的题目,列表如下:

102.二叉树的层序遍历 

107.二叉树的层次遍历II 

199.二叉树的右视图 

637.二叉树的层平均值 

429.N叉树的前序遍历 

515.在每个树行中找最大值

116. 填充每个节点的下一个右侧节点指针

117.填充每个节点的下一个右侧节点指针II

103 锯齿层序遍历

本节,我们研究最基本的层序遍历题102 、103和107 。

2.LeetCode102.二叉树的层序遍历

题目要去:给你一个二叉树,请你返回其按 层序遍历 得到的节点值。(即逐层地,从左到右访问所有节点)。

我们先看一个上面这个简单的树结构。我们提到这里可以使用队列进行广度优先遍历。广度优先遍历是按层层推进的方式,遍历每一层的节点。题目要求的是返回每一层的节点值,所以这题用广度优先来做非常合适。

2.1 迭代方式遍历

广度优先需要用队列作为辅助结构,我们先将根节点放到队列中,然后不断遍历队列。

首先拿出根节点,如果左子树/右子树不为空,就将他们放入队列中。第一遍处理完后,根节点已经从队列中拿走了,而根节点的两个孩子已放入队列中了,现在队列中就有两个节点 2 和 5。

第二次处理,会将 2 和 5 这两个节点从队列中拿走,然后再将 2 和 5 的子节点放入队列中,现在队列中就有三个节点 3,4,6。

我们把每层遍历到的节点都放入到一个结果集中,最后返回这个结果集就可以了。代码如下:

public List<List<Integer>> levelOrder(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(int i=0;i<size;++i) {        TreeNode t = queue.remove();        tmp.add(t.val);        if(t.left!=null) {          queue.add(t.left);        }        if(t.right!=null) {          queue.add(t.right);        }      }      //将临时list加入最终返回结果中      res.add(tmp);    }    return res;  }}

2.2 拓展:递归方式进行

我们开下脑洞,把这个二叉树的样子调整一下,摆成一个田字形的样子。田字形的每一层就对应一个 list。

按照深度优先的处理顺序,会先访问节点 1,再访问节点 2,接着是节点 3。

之后是第二列的 4 和 5,最后是第三列的 6。

每次递归的时候都需要带一个 index(表示当前的层数),也就对应那个田字格子中的第几行,如果当前行对应的 list 不存在,就加入一个空 list 进去。

import java.util.*;  class Solution {  public List<List<Integer>> levelOrder(TreeNode root) {    if(root==null) {      return new ArrayList<List<Integer>>();    }    //用来存放最终结果    List<List<Integer>> res = new ArrayList<List<Integer>>();    dfs(1,root,res);    return res;  }  void dfs(int index,TreeNode root, List<List<Integer>> res) {    //假设res是[ [1],[2,3] ], index是3,就再插入一个空list放到res中    if(res.size()<index) {      res.add(new ArrayList<Integer>());    }    //将当前节点的值加入到res中,index代表当前层,假设index是3,节点值是99    //res是[ [1],[2,3] [4] ],加入后res就变为 [ [1],[2,3] [4,99] ]    res.get(index-1).add(root.val);    //递归的处理左子树,右子树,同时将层数index+1    if(root.left!=null) {      dfs(index+1, root.left, res);    }    if(root.right!=null) {      dfs(index+1, root.right, res);    }  }} 

2.LeetCode 107 层序遍历II

给定一个二叉树,返回其节点值自底向上的层序遍历。(即按从叶子节点所在层到根节点所在的层,逐层从左向右遍历)

例如给定的二叉树为:

返回结果为:

[  [15,7],  [9,20],  [3]]

如果要求从上到下输出每一层的节点值,做法是很直观的,在遍历完一层节点之后,将存储该层节点值的列表添加到结果列表的尾部。这道题要求从下到上输出每一层的节点值,只要对上述操作稍作修改即可:在遍历完一层节点之后,将存储该层节点值的列表添加到结果列表的头部。

为了降低在结果列表的头部添加一层节点值的列表的时间复杂度,结果列表可以使用链表的结构,在链表头部添加一层节点值的列表的时间复杂度是 O(1)。在 Java 中,由于我们需要返回的 List 是一个接口,这里可以使用链表实现。

class Solution {    public List<List<Integer>> levelOrderBottom(TreeNode root) {        List<List<Integer>> levelOrder = new LinkedList<List<Integer>>();        if (root == null) {            return levelOrder;        }        Queue<TreeNode> queue = new LinkedList<TreeNode>();        queue.offer(root);        while (!queue.isEmpty()) {            List<Integer> level = new ArrayList<Integer>();            int size = queue.size();            for (int i = 0; i < size; i++) {                TreeNode node = queue.poll();                level.add(node.val);                TreeNode left = node.left, right = node.right;                if (left != null) {                    queue.offer(left);                }                if (right != null) {                    queue.offer(right);                }            }            levelOrder.add(0, level);        }        return levelOrder;    }}

3.LeetCode 103 二叉树的锯齿形层序遍历 

这个题的要求是:

给定一个二叉树,返回其节点值的锯齿形层序遍历。(即先从左往右,再从右往左进行下一层遍历,以此类推,层与层之间交替进行)。

例如:

给定二叉树 [3,9,20,null,null,15,7],

返回结果是:

[  [3],  [20,9],  [15,7]]

这个题也是102的变种,只是最后输出的要求有所变化,要求我们按层数的奇偶来决定每一层的输出顺序。规定二叉树的根节点为第 00 层,如果当前层数是偶数,从左至右输出当前层的节点值,否则,从右至左输出当前层的节点值。

我们依然可以沿用第 102 题的思想,修改广度优先搜索,对树进行逐层遍历,用队列维护当前层的所有元素,当队列不为空的时候,求得当前队列的长度 size,每次从队列中取出 size 个元素进行拓展,然后进行下一次迭代。

为了满足题目要求的返回值为「先从左往右,再从右往左」交替输出的锯齿形,我们可以利用「双端队列」的数据结构来维护当前层节点值输出的顺序。

双端队列是一个可以在队列任意一端插入元素的队列。在广度优先搜索遍历当前层节点拓展下一层节点的时候我们仍然从左往右按顺序拓展,但是对当前层节点的存储我们维护一个变量 isOrderLeft 记录是从左至右还是从右至左的:

如果从左至右,我们每次将被遍历到的元素插入至双端队列的末尾。

如果从右至左,我们每次将被遍历到的元素插入至双端队列的头部。

class Solution {    public List<List<Integer>> zigzagLevelOrder(TreeNode root) {        List<List<Integer>> ans = new LinkedList<List<Integer>>();        if (root == null) {            return ans;        }        Queue<TreeNode> nodeQueue = new LinkedList<TreeNode>();        nodeQueue.offer(root);        boolean isOrderLeft = true;
        while (!nodeQueue.isEmpty()) {            Deque<Integer> levelList = new LinkedList<Integer>();            int size = nodeQueue.size();            for (int i = 0; i < size; ++i) {                TreeNode curNode = nodeQueue.poll();                if (isOrderLeft) {                    levelList.offerLast(curNode.val);                } else {                    levelList.offerFirst(curNode.val);                }                if (curNode.left != null) {                    nodeQueue.offer(curNode.left);                }                if (curNode.right != null) {                    nodeQueue.offer(curNode.right);                }            }            ans.add(new LinkedList<Integer>(levelList));            isOrderLeft = !isOrderLeft;        }        return ans;    }}

4.429.N叉树的层序遍历 

看完这个题,相信你会瞬间微笑起来,先看题意:

给定一个 N 叉树,返回其节点值的层序遍历。(即从左到右,逐层遍历)。

树的序列化输入是用层序遍历,每组子节点都由 null 值分隔(参见示例)。

输入:root = [1,null,3,2,4,null,5,6]
输出:[[1],[3,2,4],[5,6]]

这个也是102的扩展,很简单的bfs,与二叉树的层序遍历基本一样,借助队列即可实现。

class Solution {    public List<List<Integer>> levelOrder(Node root) {        List<List<Integer>> value = new ArrayList<>();        Deque<Node> q = new ArrayDeque<>();        if (root != null)            q.addLast(root);        while (!q.isEmpty()) {            Deque<Node> next = new ArrayDeque<>();            List<Integer> nd = new ArrayList<>();            while (!q.isEmpty()) {                Node cur = q.pollFirst();                nd.add(cur.val);                for (Node chd : cur.children) {                    if (chd != null)                        next.add(chd);                }            }            q = next;            value.add(nd);        }        return value;    }}

5.总结

这篇开始写的时候我本来指向写前两个题,但是不小心却整了四道。从上面几个题,有何感受?其实上面几个题就是一个题是吧,面试的时候可能会遇到第二个,第三个,第四个,也可能是另外的变化,但是只要清晰掌握层次遍历的方法,其他的只是调整一下就行了。

另外一个就是队列虽然本身的题不多,但是需要对java中的队列的使用非常熟悉,否则上面的关键字不会用的话,自己实现会超级麻烦。

以上是关于二叉树2. 层次遍历之一的主要内容,如果未能解决你的问题,请参考以下文章

5.5树和二叉树——二叉树的层次遍历算法

二叉树的前序中序后序层次遍历的原理及C++代码实现

二叉树遍历的递归实现(先序中序后序和层次遍历)

网易:层次遍历二叉树

二叉树的层次遍历 II

二叉树层次遍历(包含C语言实现代码)