Pattern: Tree Breadth First Search,树上的BFS
介绍来自链接:https://www.zhihu.com/question/36738189/answer/908664455 作者:穷码农
这种模式基于宽度优先搜索(Breadth First Search (BFS)),适用于需要遍历一颗树。借助于队列数据结构,从而能保证树的节点按照他们的层数打印出来。打印完当前层所有元素,才能执行到下一层。所有这种需要遍历树且需要一层一层遍历的问题,都能用这种模式高效解决。
这种树上的BFS模式是通过把根节点加到队列中,然后不断遍历直到队列为空。每一次循环中,我们都会把队头结点拿出来(remove),然后对其进行必要的操作。在删除每个节点的同时,其孩子节点,都会被加到队列中。
识别树上的BFS模式:
- 如果你被问到去遍历树,需要按层操作的方式(也称作层序遍历)
labuladong的算法小抄 上的算法框架,值得看看:
// 计算从起点 start 到终点 target 的最近距离
int BFS(Node start, Node target) {
Queue<Node> q; // 核心数据结构:队列
Set<Node> visited; // 避免走回头路
q.offer(start); // 将起点加入队列
visited.add(start);
int step = 0; // 记录扩散的步数
while (q not empty) {
int sz = q.size();
/* 将当前队列中的所有节点向四周扩散 */
for (int i = 0; i < sz; i++) {
Node cur = q.poll();
/* 划重点:这里判断是否到达终点 */
if (cur is target)
return step;
/* 将 cur 的相邻节点加入队列 */
for (Node x : cur.adj())
if (x not in visited) {
q.offer(x);
visited.add(x);
}
}
/* 划重点:更新步数在这里 */
step++;
}
}
队列 q
就不说了,BFS 的核心数据结构;cur.adj()
泛指 cur
相邻的节点,比如说二维数组中,cur
上下左右四面的位置就是相邻节点;visited
的主要作用是防止走回头路,大部分时候都是必须的,但是像一般的二叉树结构,没有子节点到父节点的指针,不会走回头路就不需要 visited
。
可以先去了解一下宽度优先搜索算法,再做题。
队列的四组API
方式 | 抛出异常 | 有返回值,不抛出异常 | 阻塞 等待 | 超时等待 |
---|---|---|---|---|
添加 | add | offer() | put() | offer(,) |
移除 | remove | poll() | take() | poll(,) |
检测队首元素 | element | peek |
方法 | 功能 | 返回值 |
---|---|---|
add | 增加一个元索 | 如果队列已满,则抛出一个IIIegaISlabEepeplian异常 |
remove | 移除并返回队列头部的元素 | 如果队列为空,则抛出一个NoSuchElementException异常 |
element | 返回队列头部的元素 | 如果队列为空,则抛出一个NoSuchElementException异常 |
offer | 添加一个元素并返回true | 如果队列已满,则返回false |
poll | 移除并返问队列头部的元素 | 如果队列为空,则返回null |
peek | 返回队列头部的元素 | 如果队列为空,则返回null |
put | 添加一个元素 | 如果队列满,则阻塞 |
take | 移除并返回队列头部的元素 | 如果队列为空,则阻塞 |
经典题目:
1、Binary Tree Level Order Traversal (easy)
描述:
给你一个二叉树,请你返回其按 层序遍历 得到的节点值。 (即逐层地,从左到右访问所有节点)。
示例:
二叉树:[3,9,20,null,null,15,7],
3
/ \
9 20
/ \
15 7
返回其层次遍历结果:
[
[3],
[9,20],
[15,7]
]
解题思路: 层次遍历和按照顺序首先要想到的是队列数据结构。
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
class Solution {
public List<List<Integer>> levelOrder(TreeNode root) {
List<List<Integer>> res = new ArrayList<>();
if (root == null)
return res;
Queue<TreeNode> queue = new LinkedList<>(); // 用 LinkedList 实现队列数据结构
queue.offer(root); // 根节点入列
while (!queue.isEmpty()){ // 队列不为空则遍历
List<Integer> levelList = new ArrayList<>(); // 每一层的结果存放
int level = queue.size(); // 每层的节点数
for (int i = 0; i < level; i++) { // 保存下一层到队列中
if (queue.peek().left != null) {
queue.offer(queue.peek().left); // 将队头 左节点 入列
}
if (queue.peek().right != null) {
queue.offer(queue.peek().right); // 将队头 右节点 入列
}
levelList.add(queue.poll().val); // 节点出列, 保存节点的值
}
res.add(levelList);
}
return res;
}
}
2、Reverse Level Order Traversal (easy)
描述:
给定一个二叉树,返回其节点值自底向上的层次遍历。 (即按从叶子节点所在层到根节点所在的层,逐层从左向右遍历)
例如:
给定二叉树 [3,9,20,null,null,15,7],
3
/ \
9 20
/ \
15 7
返回其自底向上的层次遍历为:
[
[15,7],
[9,20],
[3]
]
**解题思路:**上一题中的结果反转,可以将下一层的结果保存到结果集的首部。从头插入使用 LinkedList
实现效率比较高,底层实现是双向链表。ArrayList
底层实现是 Object
数组,使用头插的话要挪动所有的元素,效率低。
class Solution {
public List<List<Integer>> levelOrderBottom(TreeNode root) {
LinkedList<List<Integer>> res = new LinkedList<>();
if (root == null)
return res;
Queue<TreeNode> queue = new LinkedList<>(); // 用 LinkedList 实现队列数据结构
queue.offer(root); // 根节点入列
while (!queue.isEmpty()){ // 队列不为空则遍历
int level = queue.size(); // 每层的节点数
List<Integer> levelList = new ArrayList<>(level); // 每一层的结果存放
for (int i = 0; i < level; i++) { // 保存下一层到队列中
if (queue.peek().left != null) {
queue.offer(queue.peek().left); // 将队头 左节点 入列
}
if (queue.peek().right != null) {
queue.offer(queue.peek().right); // 将队头 右节点 入列
}
levelList.add(queue.poll().val); // 节点出列, 保存节点的值
}
res.addFirst(levelList); // 下一层加入到结果的头部
}
return res;
}
}
3、Zigzag Traversal (medium)
描述:
给定一个二叉树,返回其节点值的锯齿形层次遍历。(即先从左往右,再从右往左进行下一层遍历,以此类推,层与层之间交替进行)。
例如:
给定二叉树 [3,9,20,null,null,15,7],
3
/ \
9 20
/ \
15 7
返回锯齿形层次遍历如下:
[
[3],
[20,9],
[15,7]
]
解题思路: 记录遍历的层数,偶数层的结果记录使用尾插,奇数层的结果记录使用头插。
class Solution {
public List<List<Integer>> zigzagLevelOrder(TreeNode root) {
LinkedList<List<Integer>> res = new LinkedList<>();
if (root == null)
return res;
Queue<TreeNode> queue = new LinkedList<>();
queue.offer(root);
int level = 0; // 记录层数
while (!queue.isEmpty()){
LinkedList<Integer> list = new LinkedList<>();
int size = queue.size();
for (int i = 0; i < size; i++) {
if (queue.peek().left != null){
queue.offer(queue.peek().left);
}
if (queue.peek().right != null){
queue.offer(queue.peek().right);
}
if (level % 2 == 0){ // 偶数层顺序插入
list.add(queue.poll().val);
}else { // 奇数层倒序插入
list.addFirst(queue.poll().val);
}
}
level++; // 层数 + 1
res.add(list);
}
return res;
}
}
4、Level Averages in a Binary Tree (easy)
描述:
给定一个非空二叉树, 返回一个由每层节点平均值组成的数组。
示例 1:
输入:
3
/ \
9 20
/ \
15 7
输出:[3, 14.5, 11]
解释:
第 0 层的平均值是 3 , 第1层是 14.5 , 第2层是 11 。因此返回 [3, 14.5, 11] 。
解题思路: 记录每一层节点总和 / 每一层节点数
class Solution {
public List<Double> averageOfLevels(TreeNode root) {
List<Double> res = new ArrayList<>();
if (root == null)
return res;
Queue<TreeNode> queue = new LinkedList<>(); // 队列
queue.offer(root); // 头结点入列
while(!queue.isEmpty()){
int size = queue.size(); // 每层节点数
double sum = 0; // 每层和
for (int i = 0; i < size; i++) {
if (queue.peek().left != null){
queue.offer(queue.peek().left); // 下层左节点
}
if (queue.peek().right != null){
queue.offer(queue.peek().right); // 下层右节点
}
sum += queue.poll().val; // 计算总和
}
res.add(sum / size); // 保存平均值
}
return res;
}
}
5、Minimum Depth of a Binary Tree (easy)
描述:
给定一个二叉树,找出其最小深度。
最小深度是从根节点到最近叶子节点的最短路径上的节点数量。
**说明:**叶子节点是指没有子节点的节点。
示例 1:
输入:root = [3,9,20,null,null,15,7]
输出:2
示例 2:
输入:root = [2,null,3,null,4,null,5,null,6]
输出:5
解题思路:
1、 使用宽度优先搜索,处理每层的节点时,如果有节点既没有左节点和右节点时即达到了叶子节点,直接返回该层的深度。处理完一层的节点后深度+1
class Solution {
public int minDepth(TreeNode root) {
if (root == null)
return 0;
Queue<TreeNode> queue = new LinkedList<>(); // 队列
queue.offer(root); // 头结点入列
int depth = 1; // 记录深度
while(!queue.isEmpty()){
int size = queue.size(); // 每层节点数
double sum = 0; // 每层和
for (int i = 0; i < size; i++) { // 处理一层的节点
if (queue.peek().left != null){
queue.offer(queue.peek().left); // 下层左节点
}
if (queue.peek().right != null){
queue.offer(queue.peek().right); // 下层右节点
}
// 该节点为叶子节点,直接返回深度
if (queue.peek().left == null && queue.peek().right == null){
return depth;
}
// 该节点出列
queue.poll();
}
depth++; // 一层处理完后,深度 +1
}
return depth;
}
}
2、使用递归:深度优先搜索。
- 叶子节点的定义是左孩子和右孩子都为 null 时叫做叶子节点
- 当 root 节点左右孩子都为空时,返回 1
- 当 root 节点左右孩子有一个为空时,返回不为空的孩子节点的深度
- 当 root 节点左右孩子都不为空时,返回左右孩子较小深度的节点值
class Solution {
public int minDepth(TreeNode root) {
if (root == null)
return 0;
int left = minDepth(root.left);
int right = minDepth(root.right);
//1.如果左孩子和右孩子有为空的情况,说明left和right 必然有一个为 0。直接返回 left + right + 1
//2.如果都不为空,返回较小深度+1
return root.left == null || root.right == null ? left + right + 1 : Math.min(left, right) + 1;
}
}
6、Level Order Successor (easy)
描述
给定一个二叉查找树,以及一个节点,求该节点在中序遍历的后继,如果没有则返回null
保证p是给定二叉树中的一个节点。(您可以直接通过内存地址找到p)
样例
样例 1:
输入: {1,#,2}, node with value 1
输出: 2
解释:
1
\
2
中序遍历结果:[1, 2]
样例 2:
输入: {2,1,3}, node with value 1
输出: 2
解释:
2
/ \
1 3
中序遍历结果:[1, 2, 3]
样例 3:
输入: root = [5,3,6,2,4,null,null,1], p = 6
输出: null
5
/ \
3 6
/ \
2 4
/
1
中序遍历结果:[1, 2, 3, 4, 5, 6]
1、先序遍历二叉树:
二叉树为空,则空操作。否则
(1)访问根节点
(2)先序遍历左子树
(3)先序遍历右子树
2、中序遍历二叉树:
二叉树为空,则空操作。否则
(1)中序遍历左子树
(2)访问根节点
(3)中序遍历右子树
3、后序遍历二叉树:
二叉树为空,则空操作。否则
(1)后序遍历左子树
(2)后序遍历右子树
(3)访问根节点
一棵二叉查找树(BST)定义为:
- 节点的左子树中的值要严格小于该节点的值。
- 节点的右子树中的值要严格大于该节点的值。
- 左右子树也必须是二叉查找树。
- 一个节点的树也是二叉查找树。
节点的祖先是从根节点到该节点所经分支上的所有节点。反之,以某个节点为根的子树中的任一结点都称为该节点的子孙。
遍历二叉树是以一定的规则将二叉树中的节点排成一个线性的序列,得到二叉树中的节点的先序序列或中序序列或后序序列。这实质上是对一个非线性结构进行线性化操作,使得每个节点(除第一个和最后一个外)在这些线性序列中有且仅有一个直接前驱和直接后继。例如下图中序序列为 DBHEAFCIG
, A
的前驱为E
,后继为F
。
中序遍历的后继:
- 当前节点存在右子树:那么当前节点的后继为右子节点的子树中最左端的节点。例如
B
存在右子树,则其后继为右子节点E
的最左端节点H
。 - 当前节点不存在右子树:
淦!卡了好几天不理解,先跳过,以后有时间再回来解决吧。
7、Connect Level Order Siblings (medium)
描述:
给定一个 N 叉树,返回其节点值的层序遍历。(即从左到右,逐层遍历)。
树的序列化输入是用层序遍历,每组子节点都由 null 值分隔(参见示例)。
示例:
示例 1:
输入:root = [1,null,3,2,4,null,5,6]
输出:[[1],[3,2,4],[5,6]]
示例 2:
输入:root = [1,null,2,3,4,5,null,null,6,7,null,8,null,9,10,null,null,11,null,12,null,13,null,null,14]
输出:[[1],[2,3,4,5],[6,7,8,9,10],[11,12,13],[14]]
解题思路: 宽度优先搜索+队列
/*
// Definition for a Node.
class Node {
public int val;
public List<Node> children;
public Node() {}
public Node(int _val) {
val = _val;
}
public Node(int _val, List<Node> _children) {
val = _val;
children = _children;
}
};
*/
class Solution {
public List<List<Integer>> levelOrder(Node root) {
List<List<Integer>> res = new ArrayList<>();
if (root == null)
return res;
Queue<Node> queue = new LinkedList<>(); // 队列
queue.offer(root); // 头结点入列
while (!queue.isEmpty()){
List<Integer> levelList = new ArrayList<>(); // 每一层的结果保存
int size = queue.size(); // 每一层的节点个数
for (int i = 0; i < size; i++) { // 处理一层的节点
queue.addAll(queue.peek().children); // 保存下一层的节点
levelList.add(queue.poll().val); // 保存完下一层节点,节点出列并保存其值
}
res.add(levelList); // 保存每一层的结果
}
return res;
}
}