目录
0. 前言
本文列出10道经典的树层序遍历题,
这些题都是非常简单的题,做完可以对层序遍历流程非常熟悉,同时对广度优先搜索有初步理解:
话不多说,上菜单!
107. 二叉树的层序遍历 II - 力扣(LeetCode)
515. 在每个树行中找最大值 - 力扣(LeetCode)
116. 填充每个节点的下一个右侧节点指针 - 力扣(LeetCode)
117. 填充每个节点的下一个右侧节点指针 II - 力扣(LeetCode)
1. 广度优先搜索
层序遍历是个经典的广度优先搜索题 ,一层一层地拨开你的心。
给出几个经典示例
1.1 二叉树示例
无疑就是本文题目示例:从根节点开始,到最下面一层的叶子节点,一层一层地遍历。
1.2 无向图示例
关于无向图,如果是从A节点出发,与他直接关联的3个节点B、C、D作为它的广度优先搜索的第一层。
1.3 岛屿题示例
图例来自代码随想录,展示了9宫格的“岛屿”排布。
对于中心节点,它的广度优先则是遍历它的上下左右四个方向,这是它的第一层,能够一步就到达下一步,不过也有少数情况周围8个角都算第一层,特别情况特别处理。
1.4 关键思路
对于广度优先搜索题型,看到广度两个字就要想起一种数据结构:队列!
- 先将根节点,即初始节点,第一个要访问的节点,率先放入队列。
- 然后开始循环,判断队列是否为空,不为空就循环。
- 先提前得到此时队列的size()。
- 遍历size()次:
- 先出队,对出队后的节点根据需求进行处理
- 对这个出队节点的相邻的一层节点进行判断,对于树的话,即是判断它的孩子是否为空,二叉树就判断左右两个孩子,N叉树就判断所有孩子列表,孩子节点不为空,则加入队列。
- 此轮循环遍历size()次后可以对这一层的元素做一定的处理,比如更新这一层的最大或最小值节点,计算平均值等等。
- 当队列中再也没有任何节点,返回。
上菜!直接从做题中理解思路~
第1题:102. 二叉树的层序遍历
1. 题目介绍
2. 思路
- 树判空,为空直接返回结果列表。
- 定义队列, 并将根节点3放入队列。
- 开始循环,条件为队列不为空。
- 提前获取队列中节点个数, 第一次的话, size()就是1, 只有3一个元素。
- 每一层都定义一个一维的临时列表,用于存储这一层的节点值。
- 取出队头元素, 对其进行处理。这道题是层序遍历所有节点, 则是添加值进临时列表, 其他题就要看情况灵魂变动。如何处理是层序遍历各种题型的不同之处, 这一步请注意。
这时候可以想下为什么要提前获取队列中节点个数:如果for(int i = 0; i < queue.size(); i++) 这样写会发生什么:
假如队列中有8个节点, 照理说会遍历8次, 此时:循环条件是for(;0 < 8;)
此时取出来一个节点, 队列大小实时更新为7, 因为i++, 到了下一轮:for(;1<7;)
接着又到了下一轮:for(;2<6;)、for(;3<5;)、for(;4<4;), 这不是只能遍历4次了嘛。
所以提前获取队列节点个数的目的就是锁定for循环的次数。
- 判断节点的左右孩子, 不为空就按照顺序放进队列。
- 这一轮循环完, 将已经获取了一层节点值的临时列表添加进去结果列表。
第一次做, 就走完吧。
接下来又是循环, 将9取出来, 加入临时列表, 判断节点9孩子是否为空, 都为空则不管:
接下来将节点20取出来, 加入临时列表, 判断节点20孩子不为空,加入队列。此时这一轮又遍历完,将临时列表加入到结果列表:
接下来将节点15取出来, 加入临时列表, 判断节点15孩子为空。
接下来将节点7取出来, 加入临时列表, 判断节点7孩子为空。此时这一轮又遍历完,将临时列表加入到结果列表:
最后while(!queue.isEmpty())条件为false, 闹剧结束了, return res; 轻松通关~
3. 代码
Java:
public static List<List<Integer>> levelOrder(TreeNode root) {
// 1. 定义返回结果列表
List<List<Integer>> res = new LinkedList<>();
// 2. 树为空 直接返回空列表
if(root == null) return res;
// 3. 定义队列 并将根节点入队
Queue<TreeNode> queue = new ArrayDeque<>();
queue.offer(root);
// 4. 当队列不为空, 则循环遍历
while (!queue.isEmpty()){
// 5. 提前获取队列中元素个数, 千万不能在下面的循环条件中写 i < queue.size(), 因为一直在出队, size在不断变小
int size = queue.size();
// 6. 每一轮循环都重新定义一个临时的一维列表 用于存储这一层的节点值
List<Integer> temp = new LinkedList<>();
for(int i = 0; i < size; i++){
// 7. 取出队头元素
TreeNode node = queue.poll();
// 8. 对这个节点进行处理, 这道题是层序遍历所有节点, 则是添加值进临时列表, 其他题就要看情况灵魂变动
temp.add(node.val);
// 9.左右孩子不为空 加入队列
if(node.left != null) queue.offer(node.left);
if(node.right != null) queue.offer(node.right);
}
// 10. 这一轮循环完, 将已经获取了一层节点值的临时列表添加进去结果列表
res.add(temp);
}
return res;
}
C++:
vector<vector<int>> levelOrder(TreeNode* root) {
// 1. 定义返回结果列表
vector<vector<int>> res;
// 2. 树为空 直接返回空列表
if(root == nullptr) return res;
// 3. 定义队列 并将根节点入队
queue<TreeNode*> queue;
queue.push(root);
// 4. 当队列不为空, 则循环遍历
while (!queue.empty()){
// 5. 提前获取队列中元素个数, 千万不能在下面的循环条件中写 i < queue.size(), 因为一直在出队, size在不断变小
int size = queue.size();
// 6. 每一轮循环都重新定义一个临时的一位列表 用于存储这一层的节点值
vector<int> temp;
for(int i = 0; i < size; i++){
// 7. 取出队头元素
TreeNode* node = queue.front();
queue.pop();
// 8. 对这个节点进行处理, 这道题是层序遍历所有节点, 则是添加值进临时列表, 其他题就要看情况灵魂变动
temp.push_back(node->val);
// 9.左右孩子不为空 加入队列
if(node->left != nullptr) queue.push(node->left);
if(node->right != nullptr) queue.push(node->right);
}
// 10. 这一轮循环完, 将已经获取了一层节点值的临时列表添加进去结果列表
res.push_back(temp);
}
return res;
}
JS:
const levelOrder = function(root) {
// 1. 定义返回结果列表
let res = [];
// 2. 树为空 直接返回空列表
if(root == null) return res;
// 3. 定义队列 并将根节点入队
let queue = [];
queue.push(root);
// 4. 当队列不为空, 则循环遍历
while (queue.length !== 0){
// 5. 提前获取队列中元素个数, 千万不能在下面的循环条件中写 i < queue.size(), 因为一直在出队, size在不断变小
let size = queue.length;
// 6. 每一轮循环都重新定义一个临时的一位列表 用于存储这一层的节点值
let temp = [];
for(let i = 0; i < size; i++){
// 7. 取出队头元素
let node = queue.shift();
// 8. 对这个节点进行处理, 这道题是层序遍历所有节点, 则是添加值进临时列表, 其他题就要看情况灵魂变动
temp.push(node.val);
// 9.左右孩子不为空 加入队列
if(node.left != null) queue.push(node.left);
if(node.right != null) queue.push(node.right);
}
// 10. 这一轮循环完, 将已经获取了一层节点值的临时列表添加进去结果列表
res.push(temp);
}
return res;
};
第2题:107. 二叉树的层序遍历 II
1. 题目介绍
2.思路
就是第1题每一层的结果反过来, 真是无理取闹。
第一题的结果是[[3], [9, 20], [15, 7]], 我直接两极反转--->[[15, 7], [9, 20], [3]]
Collections.reverse(res);
return res;
闹剧结束了, 直接返回前将这个二维列表倒置即可,其他代码都不用改。
第3题:199. 二叉树的右视图
1. 题目介绍
2. 思路:
还记得我说的很重要那一步吗, 取出队头之后做处理:
怎么看右视图, 这不就是要取每一行的最后一个节点吗?
我直接在取出队头节点之后判断, 最后一个才添加, 前两题是不挑的, 是个节点都要。
if (i == size - 1) res.add(node.val);
3. 代码
代码改动都不大, 就贴个Java吧。
public static List<Integer> rightSideView(TreeNode root) {
List<Integer> res = new LinkedList<>();
if (root == null)
return res;
Queue<TreeNode> queue = new ArrayDeque<>();
queue.offer(root);
while (!queue.isEmpty()) {
int size = queue.size();
for (int i = 0; i < size; i++) {
TreeNode node = queue.poll();
// 节点是这一层的最后一个才添加进结果列表
if (i == size - 1) res.add(node.val);
if (node.left != null)
queue.offer(node.left);
if (node.right != null)
queue.offer(node.right);
}
}
return res;
}
第4题:637. 二叉树的层平均值
1. 题目介绍
2. 思路
要将每一层的节点的平均值添加进去结果列表
那我就先求和,再除以这一层的节点数目不就行了吗?
这道题注意好变量类型就行了:看上面示例的那么多00000了吗,成都人出的题, 我选择Double
3. 代码
public static List<Double> averageOfLevels(TreeNode root) {
List<Double> res = new LinkedList<>();
Queue<TreeNode> queue = new ArrayDeque<>();
queue.offer(root);
while (!queue.isEmpty()) {
int size = queue.size();
// 定义这一层的临时变量, 用来求这一层节点值的总和
Double sum = (double) 0;
for (int i = 0; i < size; i++) {
TreeNode node = queue.poll();
sum += node.val;
if (node.left != null)
queue.offer(node.left);
if (node.right != null)
queue.offer(node.right);
}
// 节点总和除以这一层的节点数目, 不就是这一层的平均值吗?
res.add(sum / size);
}
return res;
}
第5题:429. N 叉树的层序遍历
1. 题目介绍
2. 思路
还记得我上面最开始讲广度优先的关键思路了吗,都是一个套路:
只需要在判断孩子是否为空那变一下就行了:
抓紧抬走!
// 从左往右依次判断它的每个孩子是否为空, 不为空添加进去队列
for (int j = 0; j < node.children.size(); j++){
// 这是每一个孩子
Node child = node.children.get(j);
for(child != null){
queue.offer(child);
}
}
3. 代码
public static List<List<Integer>> levelOrder(Node root) {
List<List<Integer>> res = new LinkedList<>();
if (root == null)
return res;
Queue<Node> queue = new ArrayDeque<>();
queue.offer(root);
while (!queue.isEmpty()){
int size = queue.size();
List<Integer> temp = new LinkedList<>();
for(int i = 0; i < size; i++){
Node node = queue.poll();
temp.add(node.val);
// 从左往右依次判断它的每个孩子是否为空, 不为空添加进去队列
for (int j = 0; j < node.children.size(); j++){
// 这是每一个孩子
Node child = node.children.get(j);
if(child != null){
queue.offer(child);
}
}
}
res.add(temp);
}
return res;
}
第6题:515. 在每个树行中找最大值
1. 题目介绍
2. 思路
无聊~还是同样的套路, 求最大值, 那么就先定义一个变量用来记录这一层的最大值, 每经过一个节点都判断是否比他大, 比我大就把老大的身份让给你。
if(maxValue < node.val){
maxValue = node.val;
}
3. 代码
public static List<Integer> largestValues(TreeNode root) {
List<Integer> res = new LinkedList<>();
if(root == null) return res;
Queue<TreeNode> queue = new ArrayDeque<>();
queue.offer(root);
while (!queue.isEmpty()){
int size = queue.size();
// 提前定义这个变量作为这一层的最大值, 先初始化为Integer的最小值
int maxValue = Integer.MIN_VALUE;
for(int i = 0; i < size; i++){
TreeNode node = queue.poll();
// 关键就在这儿啦, 比它大就更新
if(maxValue < node.val){
maxValue = node.val;
}
if(node.left != null) queue.offer(node.left);
if(node.right != null) queue.offer(node.right);
}
res.add(maxValue);
}
return res;
}
第7题:116. 填充每个节点的下一个右侧节点指针
1. 题目介绍
2. 思路
这道题很不错, 总算跟前面那些一个模板的稍微有点不同了, 但是还是不难想到。
你是否会想:我怎么能够取出一个元素的时候,让他连接上这一层的下一个元素呢。
可能能想到这种方法;
当前节点,比如说示例的节点3。
如果它的前驱节点2,不是上一层的最后一个节点1,那么就能够将节点2的next赋值为节点3。
另外一种情况,取节点4,4的前一个节点是3,同时节点3也是4的上一层的最后一个节点,那就不管。
写写代码看看吧:
public static Node connect2(Node root) {
if(root == null) return null;
Queue<Node> queue = new ArrayDeque<>();
queue.offer(root);
// 每个节点的前面一个节点
Node pre = new Node();
// 上一层的最后一个节点
Node lastEachLevel = new Node();
while (!queue.isEmpty()){
int size = queue.size();
for(int i = 0; i < size; i++){
Node node = queue.poll();
// 如果这个节点的前面一个节点 不等于 上一层的最后一个节点
if(pre != lastEachLevel){
// 连接!
pre.next = node;
}
// 此时当前节点将成为下一个节点的前驱节点
pre = node;
// 如果当前节点已经是最后一个节点
if(i == size - 1){
// 此时当前节点将会成为下一个节点的 上一层的最后一个节点
lastEachLevel = node;
}
if(node.left != null) queue.offer(node.left);
if(node.right != null) queue.offer(node.right);
}
}
return root;
}
提交逝世看:嘿,还通过了。不过太麻烦了,还要记录两个节点。
来个简单的吧:
在看第一题的时候,你是否发现了,节点9和20是同一层的节点,且节点20在节点9后面:
在当取出队头元素9的时候,节点20在哪里?节点20在队头!
此时取得队头元素20即可,别把它取出来,是取得。不是poll(),是peek()!
// node已经被取出队列 此时的队头是node的下一个节点, 直接连接即可
node.next = queue.peek();
运行下:
错求了!
嗷嗷嗷,3怎么连接上4了,3是这一层的最后一个节点,在判断节点3之前,队列里面还有4和5呢!
3是什么节点,3是最后一个节点,那么当节点不是这一层的最后一个节点的时候才能够连接队头元素。
改一下:过啦
// 如果这个节点不是当前层的最后一个节点
if(i < size - 1){
// node已经被取出队列 此时的队头是node的下一个节点, 直接连接即可
node.next = queue.peek();
}
3. 代码
public static Node connect(Node root) {
if(root == null) return null;
Queue<Node> queue = new ArrayDeque<>();
queue.offer(root);
while (!queue.isEmpty()){
int size = queue.size();
for(int i = 0; i < size; i++){
Node node = queue.poll();
// 如果这个节点不是当前层的最后一个节点
if(i < size - 1){
// node已经被取出队列 此时的队头是node的下一个节点, 直接连接即可
node.next = queue.peek();
}
if(node.left != null) queue.offer(node.left);
if(node.right != null) queue.offer(node.right);
}
}
return root;
}
第8题:117. 填充每个节点的下一个右侧节点指针
看都不用看,凑字数的,和第7题一模一样,同样的代码直接提交。
第9题:104. 二叉树的最大深度
1. 题目介绍
2. 思路
唉,简单题。求最大深度,这不就是while()循环次数吗?
public static int maxDepth_1(TreeNode root) {
int res = 0;
if (root == null) return 0;
Queue<TreeNode> queue = new ArrayDeque<>();
queue.offer(root);
while (!queue.isEmpty()) {
// while每循环一次, depth++
res++;
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);
}
}
return res;
}
这道题其实可以作为递归题的练手题目:
我们可以想下如何递归:
直接对节点的两个孩子都进行递归,如果节点为空,那么肯定深度为0;
节点不为空,那就已经至少已经有个1了,此时再去dfs这个节点,依次类推。
给个简单例子:
1. 先直接dfs节点1, 节点不为空, 再dfs节点1的孩子2和3
2.1 此时dfs节点2,还是不为空,但是它是叶子节点了,此时dfs节点2的两个假孩子,即空节点。
dfs空节点肯定是没有返回值的。
再返回上一层:节点2,即使它还是dfs返回了0,但是节点2是真真实实存在的,给个1作为补偿吧。
2.2 作为和节点2一层的节点3, 由于节点3有孩子,那么可以多dfs一层,那么节点3的深度肯定是比节点2多一个的。
3. 再返回上一层, 对于节点1, 它的两个孩子都已经dfs过一遍了, 两个孩子返回的值差距1, 那么就哪个孩子返回得多, 就将哪个孩子的深度再加个1来返回。
dfs的核心思路就是,每往下一层, 深度都加1。
public static int maxDepth(TreeNode root) {
int res = dfs(root);
return res;
}
public static int dfs(TreeNode root){
if(root == null) return 0;
return Math.max(dfs(root.left) +1, dfs(root.right) + 1);
}
写成单个函数:
public static int maxDepth(TreeNode root) {
if(root == null) return 0;
return Math.max(maxDepth(root.left) + 1, maxDepth(root.right) + 1);
}
第10题:111. 二叉树的最小深度
1. 题目介绍
2. 思路
哎哟,怎么刚做完最大深度, 又来最小深度了。
到底深度多小是最小?
嗷嗷哦嗷嗷,当第一次遇到叶子节点的时候,我直接返回不就行了吗?
记得每次循环, 返回结果res++;
光速下班!晚安么么哒~
3. 代码
public static int minDepth(TreeNode root) {
if (root == null) return 0;
int res = 0;
Queue<TreeNode> queue = new ArrayDeque<>();
queue.offer(root);
while (!queue.isEmpty()) {
int size = queue.size();
// 每循环一次, 则这一层都要遍历完,
res++;
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(node.left == null && node.right == null){
return res;
}
}
}
return res;
}
这几道题都是层序遍历非常简单的题,相信做完之后有更深的理解。
同时层序遍历是广度优先搜索算法最经典,最简单的实践。
要想学好广度优先搜索算法,还是多去做图论的题目,比如经典的岛屿问题:200. 岛屿数量
觉得我的分享有用的话,点赞收藏关注么么哒~
本猪咪不定时分享学习笔记,期望成为更好的自己!
宝宝们晚安好梦~