🎯目标:

  1. 理解二叉树层次遍历的基本原理
  2. 理解如何分别拿到每一层的元素
  3. 掌握自底向上分层遍历二叉树
  4. 掌握如何锯齿遍历二叉树
  5. 掌握如何按层遍历N叉树
  6. 掌握如何在二叉树中寻找每层的最大值和平均值
  7. 掌握如何在二叉树中输出右视图
  8. 最底层最左边

1. 层次遍历简介

广度优先在面试里出现的频率非常高,整体属于简单题,但是很多人面试遇到时就直接放弃了,实在可惜。我们本章就集中研究一下到底难在哪里。
广度优先又叫层次遍历,基本过程如下:

image.png

层次遍历就是从根节点开始,先访问根节点下面一层全部元素,再访问之后的层次,类似金字塔一样一层层访问。我们可以看到这里就是从左到右一层一层的去遍历二叉树,先访问3,之后访问1的左右子孩子 9 和20,之后分别访问9 和20的左右子孩子 [8,13]和[15,17],最后得到结果[3,9,20,8,13,15,7]。
这里的问题是怎么将遍历过的元素的子孩子保存一下呢,例如访问9时其左右子孩子8和13应该先存一下,直到处理20之后才会处理。使用队列来存储能完美解决上述问题,例如上面的图中:

1. 首先3入队。
2. 然后3出队,之后将3的左右子孩子9和10 保存到队列中。
3. 之后9出队,将9的左右子孩子8和13入队。
4. 之后20出队,将20的左右子孩子15和7入队。
5. 之后 8,13,15,7分别出队,此时都是叶子结点,只出队就行了。

该过程不复杂,如果能将树的每层次分开了,是否可以整点新花样?首先,能否将每层的元素顺序给反转一下呢?能否奇数行不变,只将偶数行反转呢?能否将输出层次从低到root逐层输出呢?再来,既然能拿到每一层的元素了,能否找到当前层最大的元素?最小的元素?最右的元素(右视图)?最左的元素(左视图)?整个层的平均值?
很明显都可以!这么折腾有啥用呢?没啥用!但这就是层次遍历的高频算法题!这就是LeetCode里经典的层次遍历题!
102.二叉树的层序遍历
107.二叉树的层次遍历II
199.二叉树的右视图
637.二叉树的层平均值
429.N叉树的前序遍历
515.在每个树行中找最大值
116.填充每个节点的下一个右侧节点指针
117.填充每个节点的下一个右侧节点指针II
103 锯齿层序遍历
除此之外,在深度优先的题目里,有些仍然会考虑层次遍历的实现方法。

2. 基本的层序遍历与变换

我们先看最简单的情况,仅仅遍历并输出全部元素,如下:

    3
   / \
  9  20
    /  \
   15   7

上面的二叉树应输出结果 [3, 9, 20, 15, 7], 方法上面已经图示了,这里看一下怎么代码实现。先访问根节点,然后将其左右子孩子放到队列里,接着继续出队,出来的元素都将其左右自孩子放到队列里,直到队列为空了就退出就行了:

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的顺序处理的,因此队列就是一个缓存的作用。
如果要你将每层的元素分开该怎么做呢?请看一下题:

2.1 二叉树的层序遍历

102. 二叉树的层序遍历 - 力扣(LeetCode)

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

二叉树:[3,9,20,null,null,15,7],
    3
   / \
  9  20
    /  \
   15   7
返回其层序遍历结果:
 [
  [3],
  [9,20],
  [15,7]
]

我们再观察执行过程图,我们先将根节点放到队列中,然后不断遍历队列。

image.png

那如何判断某一层访问完了呢?简单,用一个变量size标记一下就行了,size表示某一层的元素个数,只要出队,就将size减1,减到0就说明该层元素访问完了。当size变成0之后,这时队列中剩余元素的个数恰好就是下一层元素的个数,因此重新将size标记为下一层的元素个数就可以继续处理新的一行了,例如在上面的序列中:

1.首先拿根节点3,其左/右子结点都不为空,就将其左右放入队列中,因此此时3已经出队了,剩余元素9和20恰好就是第二层的所有结点,此时size=2。
2.继续,将9从队列中拿走,size--变成1,并将其子孩子8和13入队。之后再将20 出队,并将其子孩子15和7入队,此时再次size--,变成9了。当size=0,说明当前层已经处理完了,此时队列有四个元素,而且恰好就是下一层的元素个数。

最后,我们把每层遍历到的节点都放入到一个结果集中,将其返回就可以了:

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(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);
        }
      }
      //此时的tmp就是当前层的全部元素,用List类型的tmp保存,加入最终结果集中
      res.add(tmp);
    }
    return res;
  }
}

上面的代码是本章最重要的算法之一,也是整个算法体系的核心算法之一,与链表反转、二分查找属于同一个级别,务必认真学习!理解透彻,然后记住!
上面的算法理解了,那接下来一些列的问题就轻松搞定了。

2.2 层序遍历-自底向上

107. 二叉树的层序遍历 II - 力扣(LeetCode)

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

img

返回结果为:

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

如果要求从上到下输出每一层的节点值,做法是很直观的,在遍历完一层节点之后,将存储该层节点值的列表添加到结果列表的尾部。这道题要求从下到上输出每一层的节点值,只要对上述操作稍作修改即可,在遍历完一层节点之后,将存储该层节点值的列表添加到结果列表的头部。
为了降低在结果列表的头部添加一层节点值的列表的时间复杂度,结果列表可以使用链表的结构,在链表头部添加一层节点值的列表的时间复杂度是 O(1)。在 Java 中,由于我们需要返回的 List 是一个接口,这里可以使用链表实现。

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;
    }

2.3 二叉树的锯齿形层序遍历

103. 二叉树的锯齿形层序遍历 - 力扣(LeetCode)

LeetCode103 题,要求是:给定一个二叉树,返回其节点值的锯齿形层序遍历。(即先从左往右,再从右往左进行下一层遍历,以此类推,层与层之间交替进行)。
例如:给定二叉树 [3,9,20,null,null,15,7]

img

返回结果是:

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

这个题也是102的变种,只是最后输出的要求有所变化,要求我们按层数的奇偶来决定每一层的输出顺序。如果当前层数是偶数,从左至右输出当前层的节点值,否则,从右至左输出当前层的节点值。这里只要采用以
我们依然可以沿用第 102 题的思想,为了满足题目要求的返回值为「先从左往右,再从右往左」交替输出的锯齿形,可以利用「双端队列」的数据结构来维护当前层节点值输出的顺序。双端队列是一个可以在队列任意一端插入元素的队列。在广度优先搜索遍历当前层节点拓展下一层节点的时候我们仍然从左往右按顺序拓展,但是对当前层节点的存储我们维护一个变量 isOrderLeft 记录是从左至右还是从右至左的:

  • 如果从左至右,我们每次将被遍历到的元素插入至双端队列的末尾。
  • 从右至左,我们每次将被遍历到的元素插入至双端队列的头部。
public List<List<Integer>> zigzagLevelOrder(TreeNode root) {
        List<List<Integer>> ans = new LinkedList<List<Integer>>();
        if (root == null) {
            return ans;
        }
        Queue<TreeNode> queue = new LinkedList<TreeNode>();
        queue.offer(root);
        boolean isOrderLeft = true;
        while (!queue.isEmpty()) {
            Deque<Integer> levelList = new LinkedList<Integer>();
            int size = queue.size();
            for (int i = 0; i < size; ++i) {
                TreeNode curNode = queue.poll();
                if (isOrderLeft) {
                    levelList.offerLast(curNode.val);
                } else {
                    levelList.offerFirst(curNode.val);
                }
                if (curNode.left != null) {
                    queue.offer(curNode.left);
                }
                if (curNode.right != null) {
                    queue.offer(curNode.right);
                }
            }
            ans.add(new LinkedList<Integer>(levelList));
            isOrderLeft = !isOrderLeft;
        }
        return ans;
    }

2.4 N 叉树的层序遍历

429. N 叉树的层序遍历 - 力扣(LeetCode)

LeetCode429 给定一个 N 叉树,返回其节点值的层序遍历。(即从左到右,逐层遍历)。
树的序列化输入是用层序遍历,每组子节点都由 null 值分隔(参见示例)。

img

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

N叉树的定义如下,就是一个值,加一个列表,其类型仍然是Node:

class Node {
    public int val;
    public List<Node> children;
}

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

public List<List<Integer>> nLevelOrder(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;
    }

3. 几个处理每层元素的题目

如果我们拿到了每一层的元素,那是不是可以利用一下造几个题呢?例如每层找最大值、平均值、最右侧的值呢?当然可以。LeetCode里就有三道非常明显的题目。
515.在每个树行中找最大值(最小)
637.二叉树的层平均值
199.二叉树的右视图
既然能这么干,我们能否自己造几个题:求每层最小值可以不?求每层最左侧的可以不?我们是不是可以给LeetCode贡献几道题了?

3.1 在每个树行中找最大值

515. 在每个树行中找最大值 - 力扣(LeetCode)

LeetCode 515题目要求:给定一棵二叉树的根节点 root ,请找出该二叉树中每一层的最大值。

img

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

这里其实就是在得到一层之后使用一个变量来记录当前得到的最大值:

public static List<Integer> largestValues(TreeNode root) {
    List<Integer> res = new ArrayList<>();
    Deque<TreeNode> deque = new ArrayDeque<>();

    if (root != null) {
        deque.addLast(root);
    }

    while (!deque.isEmpty()) {
        int size = deque.size();
        int levelMaxNum = Integer.MIN_VALUE;
        for (int i = 0; i < size; i++) {
            TreeNode node = deque.poll();
            levelMaxNum = Math.max(node.val, levelMaxNum);
            if (node.left != null) {
                deque.addLast(node.left);
            }
            if (node.right != null) {
                deque.addLast(node.right);
            }
        }
        res.add(levelMaxNum);
    }
    return res;
}

3.2 在每个树行中找平均值

637. 二叉树的层平均值 - 力扣(LeetCode)

LeetCode 637 要求给定一个非空二叉树, 返回一个由每层节点平均值组成的数组。示例

img

输入:root = [3,9,20,null,null,15,7]
输出:[3.00000,14.50000,11.00000]
解释:第 0 层的平均值为 3,第 1 层的平均值为 14.5,第 2 层的平均值为 11 。
因此返回 [3, 14.5, 11] 。

这个题和前面的几个一样,只不过是每层都先将元素保存下来,最后求平均就行了:

public static List<Double> averageOfLevels(TreeNode root) {
    List<Double> res = new ArrayList<>();
    if (root == null) {
        return res;
    }
    Queue<TreeNode> list = new LinkedList<>();
    list.add(root);
    while (list.size() != 0) {
        int len = list.size();
        double sum = 0;
        for (int i = 0; i < len; i++) {
            TreeNode node = list.poll();
            sum += node.val;
            if (node.left != null) {
                list.add(node.left);
            }
            if (node.right != null) {
                list.add(node.right);
            }
        }
        res.add(sum / len);
    }
    return res;
}

3.3 二叉树的右视图

199. 二叉树的右视图 - 力扣(LeetCode)

LeetCode 199题目要求是:给定一个二叉树的根节点 root,想象自己站在它的右侧,按照从顶部到底部的顺序,返回从右侧所能看到的节点值。例如:

img

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

这个题目出现频率还挺高的,如果没有提前思考过,面试现场可能会想不到怎么做。其实也很简单那,利用 BFS 进行层次遍历,记录下每层的最后一个元素。

public static List<Integer> rightSideView(TreeNode root) {
    List<Integer> res = new ArrayList<>();
    if (root == null) {
        return res;
    }
    Queue<TreeNode> queue = new LinkedList<>();
    queue.offer(root);
    while (!queue.isEmpty()) {
        int size = queue.size();
        for (int i = 0; i < size; i++) {
            TreeNode node = queue.poll();
            if (node.left != null) {
                queue.offer(node.left);
            }
            if (node.right != null) {
                queue.offer(node.right);
            }
            if (i == size - 1) {
                res.add(node.val);
            }
        }
    }
    return res;
}

3.4 最底层最左边

513. 找树左下角的值 - 力扣(LeetCode)

上面这个层次遍历的思想可以方便的解决LeetCode513. 二叉树最底层最左边的值的问题:给定一个二叉树的 根节点root,请找出该二叉树的 最底层 最左边 节点的值。

img

输入: root = [2,1,3]
输出: 1

img

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

我们在2介绍了很多次如何使用层次遍历,这里有两个问题:该怎么知道什么时候到了最底层呢?假如最底层有两个,该怎么知道哪个是最左的呢?
我们继续观察层次遍历的执行过程:

image.png

public int findBottomLeftValue(TreeNode root) {
        if (root.left == null && root.right == null) {
            return root.val;
        }

        Queue<TreeNode> queue = new LinkedList<>();
        queue.offer(root);
        TreeNode temp = new TreeNode(-100);

        while (!queue.isEmpty()) {
            temp = queue.poll();
            if (temp.right != null) {
                // 先把右节点加入 queue
                queue.offer(temp.right);
            }
            if (temp.left != null) {
                // 再把左节点加入 queue
                queue.offer(temp.left);
            }
        }
        return temp.val;
    }

我们分析了很多题目,研究的时候你会发现所有的题目都是来自几种遍历的拓展,只不过结束条件和处理方式存在区别,除此之外还有一些质量不错的二叉树的题目,例如LeetCode404.计算给定二叉树的所有左叶子之和等等。

Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐