1.树的层次遍历
广度优先在面试里出现的频率非常高,整体属于简单题。
广度优先又叫层次遍历,基本过程如下:
层次遍历就是从根节点开始,先访问根节点下面一层全部元素,再访问之后的层次,类似金字塔一样一层层访问。我们可以看到这里就是从左到右一层一层的去遍历二叉树,先访问3,之后访问1的左右子孩子 9 和10,之后分别访问9 和20的左右子孩子 [4,5]和[6,7],最后得到结果[3,9,20,8,13,15,7]。
这里的问题是怎么将遍历过的元素的子孩子保存一下呢,例如访问9时其左右子孩子8和13应该先存一下,直到处理20之后才会处理。使用队列来存储能完美解决上述问题,例如上面的图中:
首先3入队。
然后3出队,之后将3的左右子孩子9和10 保存到队列中。
之后9出队,将9的左右子孩子8和13入队。
之后20出队,将20的左右子孩子15和7入队。
之后 8,13,15,7分别出队,此时都是叶子结点,只出队就行了。
该过程不复杂,如果能将树的每层次分开了,是否可以整点新花样?首先,能否将每层的元素顺序给反转一下呢?能否奇数行不变,只将偶数行反转呢?能否将输出层次从低到root逐层输出呢?再来,既然能拿到每一层的元素了,能否找到当前层最大的元素?最小的元素?最右的元素(右视图)?最左的元素(左视图)?整个层的平均值?
1.1 构建二叉树
BinaryTree
public class BinaryTree {
public TreeNode root;
public BinaryTree() {
root = null;
}
/**
* 方法1:比较粗糙的创建二叉树关系
*/
public TreeNode buildBinaryTree() {
TreeNode node = new TreeNode(3);
node.left = new TreeNode(9);
node.right = new TreeNode(20);
node.right.right = new TreeNode(7);
node.right.left = new TreeNode(15);
node.right.left.left = new TreeNode(16);
node.right.left.right = new TreeNode(17);
return node;
}
}
TreeNode
public class TreeNode {
public int val;
public TreeNode left;
public TreeNode right;
public TreeNode(int val) {
this.val = val;
}
TreeNode(int val, TreeNode left, TreeNode right) {
this.val = val;
this.left = left;
this.right = right;
}
}
1.2 构建 N 叉树
NTree
public class NTree {
public NTreeNode root;
public NTree() {
root = null;
}
/**
* 方法1:比较粗糙的创建二叉树关系
*/
public NTreeNode buildNTree() {
//子数
List<NTreeNode> sub = new ArrayList<NTreeNode>();
sub.add(new NTreeNode(5));
sub.add(new NTreeNode(6));
List<NTreeNode> nodes = new ArrayList<NTreeNode>();
nodes.add(new NTreeNode(3, sub));
nodes.add(new NTreeNode(2));
nodes.add(new NTreeNode(4));
NTreeNode root = new NTreeNode(3, nodes);
return root;
}
NTreeNode
public class NTreeNode {
public int val;
public List<NTreeNode> children;
NTreeNode(int val) {
this.val = val;
}
public NTreeNode(int _val, List<NTreeNode> _children) {
val = _val;
children = _children;
}
}
2. 基本的层序遍历与变换
2.1 树的基本层序遍历
我们先看最简单的情况,仅仅遍历并输出全部元素,如下:
3
/ \
9 20
/ \
15 7
上面的二叉树应输出结果 [3, 9, 20, 15, 7], 方法上面已经图示了,这里看一下怎么代码实现。先访问根节点,然后将其左右子孩子放到队列里,接着继续出队,出来的元素都将其左右自孩子放到队列里,直到队列为空了就退出就行了:
思路:
定义一个
ArrayList
保存结果,然后使用LinkedList
作为队列,为什么要 LinkedList 当作队列?看后面将根节点入队,然后根据 节点的数量进行循环,因为第一次保存了根节点,所以长度为1, 然后在循环中将 队列出队,将
val
写入res
(结果),然后判断 此节点有没有度
(子节点),如果有,则让它的子节点入队(从左到右),那么到下次循环它的 size 也会变化,这样依次从上到下。为什么使用 LinkedList 当作队列?
LinkedList<TreeNode>
是一个既可以作为队列使用,又可以作为链表使用的数据结构。在 Java 中,
LinkedList
类实现了List
接口和Queue
接口,因此可以同时具备链表和队列的功能。它可以用作普通的链表,支持插入、删除和访问元素等操作,也可以用作队列,支持先进先出 (FIFO) 的特性,即支持在队列的一端插入元素,在另一端删除元素。在这种情况下,
LinkedList<TreeNode>
可以用作一个队列,用于存储TreeNode
类型的元素,并按照先进先出的顺序进行操作。可以使用add
方法将元素添加到队列的末尾,使用remove
方法从队列的头部移除元素,并使用其他队列相关的方法进行操作,如peek
、poll
等。同时,
LinkedList<TreeNode>
也可以像链表一样进行操作,比如使用add
、remove
、get
等方法对元素进行插入、删除和访问操作。因此,
LinkedList
既可以作为队列使用,也可以作为链表使用,具体取决于在代码中如何使用它。
代码:
public static List<Integer> printBinaryTree(TreeNode root) {
if (root == null) {
return null;
}
// 保存结果和每层的元素
ArrayList<Integer> res = new ArrayList<>();
LinkedList<TreeNode> nodes = new LinkedList<>();
// 先把根节点保存进队列中
nodes.add(root);
// 遍历每层节点,直到没有节点为止
while (nodes.size() > 0) {
// 取出节点,队列中出队
TreeNode tree = nodes.remove();
// 保存进结果中
res.add(tree.val);
// 判断是否有左右孩子,保存到队列中
if (tree.left != null) {
nodes.add(tree.left);
}
if (tree.right != null) {
nodes.add(tree.right);
}
}
return res;
}
NC: 根据树的结构可以看到,一个结点在一层访问之后,其子孩子都是在下层按照FIFO的顺序处理的,因此队列就是一个缓存的作用。 如果要你将每层的元素分开该怎么做呢?请看一下题:
2.2 二叉树的层序遍历
LeetCode102 题目要求:给你一个二叉树,请你返回其按层序遍历得到的节点值。(即逐层地,从左到右访问所有节点)。
二叉树:
3
/ \
9 20
/ \
15 7
返回的结果:
[
[3],
[9,20],
[15,7]
]
思路:
大致思路和 2.1 差不多,都是从上而下,但是其中有些步骤是不一样的。 在循环中每次获取 nodes(队列)的长度保存在
size
中,size
表示某一层的元素个数,根据每层的数量再进行循环,每次size++
,最后 等到 size == 队列长度的时候,该层就遍历完了,这时nodes
里的元素又是下一层的所有元素,下次循环中size = nodes.size
,依次即可。最后,把每层遍历到的节点都放入到一个结果集中,将其返回就可以了。
代码:
public static List<List<Integer>> printTree(TreeNode root) {
if (root == null) {
return null;
}
// 保存结果和每层的元素
ArrayList<List<Integer>> res = new ArrayList<>();
LinkedList<TreeNode> nodes = new LinkedList<>();
// 先把根节点保存进队列中
nodes.add(root);
// 遍历每层节点,直到没有节点为止
while (nodes.size() > 0) {
// 获取当前队列中元素个数
int size = nodes.size();
ArrayList<Integer> temp = new ArrayList<>();
// 根据每层节点个数,将每层的所以元素都保存进 temp 中
for (int i = 0; i < size; i++) {
// 当前节点出队列,写入到 res 中
TreeNode tree = nodes.remove();
temp.add(tree.val);
// 判断是否有左右孩子,保存到队列中
if (tree.left != null) {
nodes.add(tree.left);
}
if (tree.right != null) {
nodes.add(tree.right);
}
}
// 最后将 temp 保存进 res 中
res.add(temp);
}
return res;
}
2.3 层序遍历-自底向上
LeetCode 107.给定一个二叉树,返回其节点值自底向上的层序遍历。(即按从叶子节点所在层到根节点所在的层,逐层从左向右遍历)。例如给定的二叉树为:
二叉树:
3
/ \
9 20
/ \
15 7
返回的结果:
[
[15,7]
[9,20],
[3],
]
思路:
使用链表来保存结果,每层遍历完之后,插入到链表的头部,就像栈一样,把结果值反转了一遍。 在链表头部添加一层节点值的列表的时间复杂度是 O(1)。在 Java 中,由于我们需要返回的 List 是一个接口,这里可以使用链表实现。
代码:
public static List<List<Integer>> printTree(TreeNode root) {
if (root == null) {
return null;
}
// 利用链表可以实现时间复杂度 O(1),数组还要将后面元素后移
LinkedList<List<Integer>> res = new LinkedList<>();
LinkedList<TreeNode> nodes = new LinkedList<>();
// 根节点保存进队列中
nodes.add(root);
while (nodes.size() > 0) {
int size = nodes.size();
// 保存每层节点
ArrayList<Integer> temp = new ArrayList<>();
for (int i = 0; i < size; i++) {
// 每次取出队列的节点
TreeNode treeNode = nodes.remove();
// 保存进 temp 变量中
temp.add(treeNode.val);
// 判断是否有左右孩子,保存到队列中
if (treeNode.left != null) {
nodes.add(treeNode.left);
}
if (treeNode.right != null) {
nodes.add(treeNode.right);
}
}
// 将每层遍历的结果保存进 res 中,
// 要头插,像栈一样
res.addFirst(temp);
}
return res;
}
2.4 二叉树的锯齿形层序遍历
LeetCode103 题,要求是:给定一个二叉树,返回其节点值的锯齿形层序遍历。(即先从左往右,再从右往左进行下一层遍历,以此类推,层与层之间交替进行)。
二叉树:
3
/ \
9 20
/ \
15 7
返回的结果:
[
[3],
[20,9,],
[15,7]
]
思路:
为了满足题目要求的返回值为「先从左往右,再从右往左」交替输出的锯齿形,使用一个 flag 开关,默认为 false(第一层不需要转),在判断每层子节点的时候,如果 为 true 那么就先从右孩子开始入队(锯齿),每层遍历完 flag = !flag
代码:
public static List<List<Integer>> printTree(TreeNode root) {
LinkedList<List<Integer>> res = new LinkedList<>();
Queue<TreeNode> nodes = new LinkedList<>();
nodes.offer(root);
// 判断条件
boolean flag = false;
while (nodes.size() > 0) {
int size = nodes.size();
ArrayList<Integer> temp = new ArrayList<>();
for (int i = 0; i < size; i++) {
// 取出队列第一个节点,保存进 temp 中
TreeNode treeNode = nodes.poll();
temp.add(treeNode.val);
// 如果为 false 则 先让右节点入队
if (flag) {
if (treeNode.left != null) {
nodes.add(treeNode.left);
}
if (treeNode.right != null) {
nodes.add(treeNode.right);
}
} else {
if (treeNode.right != null) {
nodes.add(treeNode.right);
}
if (treeNode.left != null) {
nodes.add(treeNode.left);
}
}
}
res.add(temp);
flag = !flag;
}
return res;
}
2.5 N 叉树的层序遍历
LeetCode429 给定一个 N 叉树,返回其节点值的层序遍历。(即从左到右,逐层遍历)。 树的序列化输入是用层序遍历,每组子节点都由 null 值分隔(参见示例)。
N叉树:
3
/ | \3 2 4
/ \
15 7
返回的结果:
[
[3],
[3,2,4],
[15,7]
]
思路:
还是跟 2.2 思路差不多,不同的就是需要在每层判断是否有下一个节点(容易出现空指针)
代码:
public static List<List<Integer>> printTreeNode(NTreeNode root) {
if (root == null) {
return null;
}
LinkedList<List<Integer>> res = new LinkedList<>();
Queue<NTreeNode> nodes = new LinkedList<>();
nodes.offer(root);
while (nodes.size() > 0) {
int size = nodes.size();
ArrayList<Integer> temp = new ArrayList<>();
for (int i = 0; i < size; i++) {
NTreeNode nTreeNode = nodes.poll();
temp.add(nTreeNode.val);
// 获取当前节点的子节点,进行判空
List<NTreeNode> children = nTreeNode.children;
if (children == null || children.size() == 0) {
continue;
}
// 把当前层的下一层所有节点保存进栈中
for (NTreeNode node: nTreeNode.children) {
if (node != null) {
nodes.add(node);
}
}
}
res.add(temp);
}
return res;
}
3. 几个处理每层元素的题目
3.1 在每个树行中找最大值
LeetCode 515题目要求:给定一棵二叉树的根节点 root ,请找出该二叉树中每一层的最大值。
二叉树:
3
/ \
9 20
/ \
15 7
返回的结果:
[3,20,15]
这里其实就是在得到一层之后使用一个变量来记录当前得到的最大值
代码:
public static List<Integer> printMaxLayer(TreeNode root) {
if (root == null) {
return null;
}
ArrayList<Integer> res = new ArrayList<>();
Queue<TreeNode> nodes = new LinkedList<>();
nodes.offer(root);
while (nodes.size() > 0) {
int size = nodes.size();
int max = 0 ;
LinkedList<Integer> temp = new LinkedList<>();
for (int i = 0; i < size; i++) {
TreeNode treeNode = nodes.poll();
if (max < treeNode.val) {
max = treeNode.val;
}
if (treeNode.left != null) {
nodes.add(treeNode.left);
}
if (treeNode.right != null) {
nodes.add(treeNode.right);
}
}
res.add(max);
}
return res;
}
3.2 在每个树行中找平均值
LeetCode 637 要求给定一个非空二叉树, 返回一个由每层节点平均值组成的数组。示例
二叉树:
3
/ \
9 20
/ \
15 7
返回的结果:
[3.0,14.5,11.0]
这个题和前面的几个一样,只不过是每层都先将元素保存下来,最后求平均就行了:
代码:
public static List<Double> printAverageLayer(TreeNode root) {
if (root == null) {
return null;
}
ArrayList<Double> res = new ArrayList<>();
Queue<TreeNode> nodes = new LinkedList<>();
nodes.offer(root);
while (nodes.size() > 0) {
int size = nodes.size();
double avg = 0;
LinkedList<Integer> temp = new LinkedList<>();
for (int i = 0; i < size; i++) {
TreeNode treeNode = nodes.poll();
temp.add(treeNode.val);
if (treeNode.left != null) {
nodes.add(treeNode.left);
}
if (treeNode.right != null) {
nodes.add(treeNode.right);
}
}
double sum =0;
for (Integer integer : temp) {
sum+=integer;
}
res.add(sum/temp.size());
}
return res;
}
3.3 二叉树的右视图
LeetCode 199题目要求是:给定一个二叉树的根节点 root,想象自己站在它的右侧,按照从顶部到底部的顺序,返回从右侧所能看到的节点值。
其实就是将每层的最后一个节点保存起来
代码:
public static List<Integer> printTreeRight(TreeNode root) {
if (root == null) {
return null;
}
ArrayList<Integer> res = new ArrayList<>();
Queue<TreeNode> nodes = new LinkedList<>();
nodes.offer(root);
while (nodes.size() > 0) {
int size = nodes.size();
for (int i = 0; i < nodes.size(); i++) {
TreeNode treeNode = nodes.poll();
// 保存每层最后一个元素
if (i+1 == size) {
res.add(treeNode.val);
}
if (treeNode.left != null) {
nodes.offer(treeNode.left);
}
if (treeNode.right != null) {
nodes.offer(treeNode.right);
}
}
}
return res;
}
3.4 二叉树的左视图
做法还是跟上面一样的,找到每层的第一个节点即可
代码:
private static List<Integer> printTreeRight(TreeNode root) {
if (root == null) {
return null;
}
ArrayList<Integer> res = new ArrayList<>();
Queue<TreeNode> nodes = new LinkedList<>();
nodes.offer(root);
while (nodes.size() > 0) {
int size = nodes.size();
for (int i = 0; i < size; i++) {
TreeNode treeNode = nodes.poll();
if (i == 0) {
res.add(treeNode.val);
}
if (treeNode.left != null) {
nodes.add(treeNode.left);
}
if (treeNode.right != null) {
nodes.add(treeNode.right);
}
}
}
return res;
}
3.5 最底层最左边
3.3 这个层次遍历的思想可以方便的解决剑指 Offer II 045. 二叉树最底层最左边的值的问题:给定一个二叉树的 根节点root,请找出该二叉树的 最底层 最左边 节点的值。
方法1:
思路:
定义一个变量,在遍历每一层的时候,保存每一层的第一个节点,当遍历完的时候,这个变量就是最后一层的第一个元素
代码:
public static Integer printLowerLeft(TreeNode root) {
if (root == null) {
return null;
}
int res = Integer.MIN_VALUE;
Queue<TreeNode> nodes = new LinkedList<>();
nodes.offer(root);
while (nodes.size() > 0) {
int size = nodes.size();
for (int i = 0; i < size; i++) {
TreeNode treeNode = nodes.poll();
if (i == 0) {
res = treeNode.val;
}
if (treeNode.left != null) {
nodes.offer(treeNode.left);
}
if (treeNode.right != null) {
nodes.offer(treeNode.right);
}
}
}
return res;
}
方法2:
思路:
将原有树的每层都反转,使用队列保存元素入队的信息,然后依次出栈,直到最后一个为止,输出最后一个元素就是 最底层左边元素
二叉树:
3 3
/ \ / \
9 20 ---- > 20 9
/ \ / \
15
7 715
代码:
public static Integer printLowerLeft2(TreeNode root) {
if (root == null) {
return null;
}
Queue<TreeNode> nodes = new LinkedList<>();
nodes.offer(root);
// 保存结果
TreeNode res = new TreeNode(-1);
while (nodes.size() > 0) {
// 保存每次的节点
res = nodes.poll();
// 从右分支开始入队
if (res.right != null) {
nodes.offer(res.right);
}
if (res.left != null) {
nodes.offer(res.left);
}
}
// 循环到最后一个节点,就是想要的结果
return res.val;
}