二叉树的层序遍历(自上而下)
目前只掌握了层序遍历的迭代方式
题目描述
给你二叉树的根节点 root
,返回其节点值的 层序遍历 。 (即逐层地,从左到右访问所有节点)。
示例 1:
输入:root = [3,9,20,null,null,15,7]
输出:[[3],[9,20],[15,7]]
示例 2:
输入:root = [1]
输出:[[1]]
示例 3:
输入:root = []
输出:[]
提示:
- 树中节点数目在范围
[0, 2000]
内 -1000 <= Node.val <= 1000
解题思路
层序遍历一个二叉树。就是从左到右一层一层的去遍历二叉树。这种遍历的方式和我们之前讲过的都不太一样。
需要借用一个辅助数据结构即队列来实现,队列先进先出,符合一层一层遍历的逻辑,而用栈先进后出适合模拟深度优先遍历也就是递归的逻辑。
而这种层序遍历方式就是图论中的广度优先遍历,只不过我们应用在二叉树上。
使用队列实现二叉树广度优先遍历,动画如下:
这样就实现了层序从左到右遍历二叉树。
代码如下:这份代码也可以作为二叉树层序遍历的模板
简洁形式
class Solution {
public List<List<Integer>> levelOrder(TreeNode root) {
List<List<Integer>> resList = new ArrayList<>();
if (root == null) {
return resList; // 如果根节点为空,直接返回空的结果列表
}
Queue<TreeNode> que = new LinkedList<>();
que.offer(root); // 将根节点加入到队列中
while (!que.isEmpty()) { // 队列不为空时进行循环
List<Integer> itemList = new ArrayList<>(); // 当前层的节点值列表
int len = que.size(); // 当前层的节点数
for (int i = 0; i < len; i++) {
TreeNode tmpNode = que.poll(); // 获取并移除队列头节点
itemList.add(tmpNode.val); // 将节点值加入到当前层的节点值列表中
if (tmpNode.left != null) {
que.offer(tmpNode.left); // 将左子节点加入到队列中
}
if (tmpNode.right != null) {
que.offer(tmpNode.right); // 将右子节点加入到队列中
}
}
resList.add(itemList); // 将当前层的节点值列表加入到结果列表中
}
return resList; // 返回结果列表
}
}
用函数:
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class Solution {
public List<List<Integer>> resList = new ArrayList<List<Integer>>();
public List<List<Integer>> levelOrder(TreeNode root) {
// checkFun01(root,0);
checkFun02(root);// 把根节点作为参数传入方法中
return resList;
}
// BFS--迭代方式--借助队列
public void checkFun02(TreeNode node) { // 把根节点作为参数传入方法中
if (node == null)
return; // 节点为null说明遍历完毕了,直接return
Queue<TreeNode> que = new LinkedList<TreeNode>(); // 新建一个队列que;用于存放二叉树的节点 多态【父类的引用指向实现类对象】
que.offer(node); // 将节点node插入此队列 que
while (!que.isEmpty()) { // 队列不为空的时候操作队列,进行存取一系列操作
List<Integer> itemList = new ArrayList<Integer>();// 把二叉树的每一层元素(的值)放入整数集合itemList
int len = que.size();// 队列的元素数目(即节点数目)
while (len > 0) {
TreeNode tmpNode = que.poll(); // 获取并移除此列表的头(第一个元素--->节点)
itemList.add(tmpNode.val); // 将节点的值加入到整数集合itemList中
if (tmpNode.left != null) // 若该节点左节点不为空,则将左节点加入到队列中
que.offer(tmpNode.left);
if (tmpNode.right != null)
que.offer(tmpNode.right); // 若该节点右节点不为空,则将左节点加入到队列中
len--;
}
resList.add(itemList); // 走到这一步说明二叉树当前层已经遍历完了,存放在整数集合itemList中,然后将itemList放入结果集resList中
}
}
}
参考解题
// 102.二叉树的层序遍历
class Solution {
public List<List<Integer>> resList = new ArrayList<List<Integer>>();
public List<List<Integer>> levelOrder(TreeNode root) {
//checkFun01(root,0);
checkFun02(root);
return resList;
}
//BFS--迭代方式--借助队列
public void checkFun02(TreeNode node) {
if (node == null) return;//节点为null说明遍历完毕了,直接return
Queue<TreeNode> que = new LinkedList<TreeNode>();// 新建一个队列que;用于存放二叉树的节点 多态【父类的引用指向实现类对象】
que.offer(node);
while (!que.isEmpty()) {// 队列不为空的时候操作队列,进行存取一系列操作
List<Integer> itemList = new ArrayList<Integer>();
int len = que.size();// 队列的元素数目(即节点数目)
while (len > 0) {
TreeNode tmpNode = que.poll();// 获取并移除此列表的头(第一个元素--->节点)
itemList.add(tmpNode.val);
if (tmpNode.left != null) que.offer(tmpNode.left);// 若该节点左节点不为空,则将左节点加入到队列中
if (tmpNode.right != null) que.offer(tmpNode.right);// 若该节点右节点不为空,则将左节点加入到队列中
len--;
}
resList.add(itemList); // 走到这一步说明二叉树当前层已经遍历完了,存放在整数集合itemList中,然后将itemList放入结果集resList中
}
}
//DFS--递归方式
public void checkFun01(TreeNode node, Integer deep) {
if (node == null) return;
deep++;
if (resList.size() < deep) {
//当层级增加时,list的Item也增加,利用list的索引值进行层级界定
List<Integer> item = new ArrayList<Integer>();
resList.add(item);
}
resList.get(deep - 1).add(node.val);
checkFun01(node.left, deep);
checkFun01(node.right, deep);
}
}
补充
多态中成员访问的特点
Queue
二叉树的层序遍历(自下而上)
题目描述
给你二叉树的根节点 root
,返回其节点值 自底向上的层序遍历 。 (即按从叶子节点所在层到根节点所在的层,逐层从左向右遍历)
示例 1:
输入:root = [3,9,20,null,null,15,7]
输出:[[15,7],[9,20],[3]]
示例 2:
输入:root = [1]
输出:[[1]]
示例 3:
输入:root = []
输出:[]
提示:
- 树中节点数目在范围
[0, 2000]
内 -1000 <= Node.val <= 1000
相对于102.二叉树的层序遍历,就是最后把result数组反转一下就可以了。
自己解题
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class Solution {
public List<List<Integer>> resList = new ArrayList<List<Integer>>();// 原来的结果集
List<List<Integer>> result = new ArrayList<>();// 反转的结果集
public List<List<Integer>> levelOrderBottom(TreeNode root) {
// checkFun01(root,0);
checkFun02(root);// 把根节点作为参数传入方法中
return result;
}
// BFS--迭代方式--借助队列
public List<List<Integer>> checkFun02(TreeNode node) { // 把根节点作为参数传入方法中
if (node == null)
return result; // 节点为null说明遍历完毕了,直接return(修改----按照函数返回值类型做修改)
Queue<TreeNode> que = new LinkedList<TreeNode>(); // 新建一个队列que;用于存放二叉树的节点 多态【父类的引用指向实现类对象】
que.offer(node); // 将节点node插入此队列 que
while (!que.isEmpty()) { // 队列不为空的时候操作队列,进行存取一系列操作
List<Integer> itemList = new ArrayList<Integer>();// 把二叉树的每一层元素(的值)放入整数集合itemList
int len = que.size();// 队列的元素数目(即节点数目)
while (len > 0) {
TreeNode tmpNode = que.poll(); // 获取并移除此列表的头(第一个元素--->节点)
itemList.add(tmpNode.val); // 将节点的值加入到整数集合itemList中
if (tmpNode.left != null) // 若该节点左节点不为空,则将左节点加入到队列中
que.offer(tmpNode.left);
if (tmpNode.right != null)
que.offer(tmpNode.right); // 若该节点右节点不为空,则将左节点加入到队列中
len--;
}
resList.add(itemList); // 走到这一步说明二叉树当前层已经遍历完了,存放在整数集合itemList中,然后将itemList放入结果集resList中
}
for (int i = resList.size() - 1; i >= 0; i--) {//做反转
result.add(resList.get(i));
}
return result;
}
}
参考解题
// 107. 二叉树的层序遍历 II
public class N0107 {
/**
* 解法:队列,迭代。
* 层序遍历,再翻转数组即可。
*/
public List<List<Integer>> solution1(TreeNode root) {
List<List<Integer>> list = new ArrayList<>();
Deque<TreeNode> que = new LinkedList<>();
if (root == null) {
return list;
}
que.offerLast(root);
while (!que.isEmpty()) {
List<Integer> levelList = new ArrayList<>();
int levelSize = que.size();
for (int i = 0; i < levelSize; i++) {
TreeNode peek = que.peekFirst();
levelList.add(que.pollFirst().val);
if (peek.left != null) {
que.offerLast(peek.left);
}
if (peek.right != null) {
que.offerLast(peek.right);
}
}
list.add(levelList);
}
List<List<Integer>> result = new ArrayList<>();
for (int i = list.size() - 1; i >= 0; i-- ) {
result.add(list.get(i));
}
return result;
}
}
补充
根据方法的返回值类型做修改,提前将反转集合定义在主方法中
for (int i = resList.size() - 1; i >= 0; i--) {//做反转
result.add(resList.get(i));
}
return result;
反转二叉树
题目描述
给你一棵二叉树的根节点 root
,翻转这棵二叉树,并返回其根节点。
示例 1:
输入:root = [4,2,7,1,3,6,9]
输出:[4,7,2,9,6,3,1]
示例 2:
输入:root = [2,1,3]
输出:[2,3,1]
示例 3:
输入:root = []
输出:[]
提示:
- 树中节点数目范围在
[0, 100]
内 -100 <= Node.val <= 100
想要翻转它,其实就把每一个节点的左右孩子交换一下就可以了。
关键在于遍历顺序,前中后序应该选哪一种遍历顺序? (一些同学这道题都过了,但是不知道自己用的是什么顺序)
遍历的过程中去翻转每一个节点的左右孩子就可以达到整体翻转的效果。
注意只要把每一个节点的左右孩子翻转一下,就可以达到整体翻转的效果
使用前序遍历和后序遍历都可以,唯独中序遍历不方便,因为中序遍历会把某些节点的左右孩子翻转了两次!建议拿纸画一画,就理解了
那么层序遍历可以不可以呢?依然可以的!只要把每一个节点的左右孩子翻转一下的遍历方式都是可以的!
递归
对于二叉树的递归法的前中后序遍历,已经在二叉树:前中后序递归遍历 (opens new window)详细讲解了。
我们下文以前序遍历为例,通过动画来看一下翻转的过程:
我们来看一下递归三部曲:
1.确定递归函数的参数和返回值
参数就是要传入节点的指针,不需要其他参数了,通常此时定下来主要参数,如果在写递归的逻辑中发现还需要其他参数的时候,随时补充。
public TreeNode invertTree(TreeNode root)
返回值的话其实也不需要,但是题目中给出的要返回root节点的指针,可以直接使用题目定义好的函数,所以就函数的返回类型为TreeNode
2.确定终止条件
当前节点为空的时候,就返回
if (root == null) {
return null;
}
3.确定单层递归的逻辑
因为是先前序遍历,所以先进行交换左右孩子节点,然后反转左子树,反转右子树。
TreeNode tmp = root.left;
root.left = root.right;
root.right = tmp;
递归:
//DFS递归
class Solution {
/**
* 前后序遍历都可以
* 中序不行,因为先左孩子交换孩子,再根交换孩子(做完后,右孩子已经变成了原来的左孩子),再右孩子交换孩子(此时其实是对原来的左孩子做交换)
*/
public TreeNode invertTree(TreeNode root) {
if (root == null) {
return null;
}
invertTree(root.left);
invertTree(root.right);
swapChildren(root);
return root;
}
private void swapChildren(TreeNode root) {
TreeNode tmp = root.left;
root.left = root.right;
root.right = tmp;
}
}
迭代:
//BFS 迭代
class Solution {
public TreeNode invertTree(TreeNode root) {
if (root == null) {return null;}
ArrayDeque<TreeNode> deque = new ArrayDeque<>();
deque.offer(root);
while (!deque.isEmpty()) {
int size = deque.size();
while (size-- > 0) {
TreeNode node = deque.poll();
swap(node);
if (node.left != null) deque.offer(node.left);
if (node.right != null) deque.offer(node.right);
}
}
return root;
}
public void swap(TreeNode root) {
TreeNode temp = root.left;
root.left = root.right;
root.right = temp;
}
广度优先遍历:层序遍历
也就是层序遍历,层数遍历也是可以翻转这棵树的,因为层序遍历也可以把每个节点的左右孩子都翻转一遍,代码如下:
public class Solution {
public TreeNode invertTree(TreeNode root) {
Queue<TreeNode> queue = new LinkedList<>();
if (root != null) {
queue.add(root);
}
while (!queue.isEmpty()) {
int size = queue.size();
for (int i = 0; i < size; i++) {
TreeNode node = queue.poll();
// Swap left and right children
TreeNode temp = node.left;
node.left = node.right;
node.right = temp;
// Add children to the queue
if (node.left != null) {
queue.add(node.left);
}
if (node.right != null) {
queue.add(node.right);
}
}
}
return root;
}
}
补充
解题之前一定要想清楚究竟是前中后序遍历,还是层序遍历。
对称二叉树
题目描述
给你一个二叉树的根节点 root
, 检查它是否轴对称。
示例 1:
输入:root = [1,2,2,3,4,4,3]
输出:true
示例 2:
输入:root = [1,2,2,null,3,null,3]
输出:false
提示:
- 树中节点数目在范围
[1, 1000]
内 -100 <= Node.val <= 100
首先想清楚,判断对称二叉树要比较的是哪两个节点,要比较的可不是左右节点!
对于二叉树是否对称,要比较的是根节点的左子树与右子树是不是相互翻转的,理解这一点就知道了其实我们要比较的是两个树(这两个树是根节点的左右子树),所以在递归遍历的过程中,也是要同时遍历两棵树。
那么如何比较呢?
比较的是两个子树的里侧和外侧的元素是否相等。如图所示:
那么遍历的顺序应该是什么样的呢?
本题遍历只能是“后序遍历”,因为我们要通过递归函数的返回值来判断两个子树的内侧节点和外侧节点是否相等。
正是因为要遍历两棵树而且要比较内侧和外侧节点,所以准确的来说是一个树的遍历顺序是左右中,一个树的遍历顺序是右左中。
递归法
递归三部曲
1.确定递归函数的参数和返回值==
因为我们要比较的是根节点的两个子树是否是相互翻转的,进而判断这个树是不是对称树,所以要比较的是两个树,参数自然也是左子树节点和右子树节点。
返回值自然是bool类型。
代码如下:
private boolean compare(TreeNode left, TreeNode right)
2.确定终止条件
要比较两个节点数值相不相同,首先要把两个节点为空的情况弄清楚!否则后面比较数值的时候就会操作空指针了。
节点为空的情况有:(注意我们比较的其实不是左孩子和右孩子,所以如下我称之为左节点右节点)
- 左节点为空,右节点不为空,不对称,return false
- 左不为空,右为空,不对称 return false
- 左右都为空,对称,返回true
此时已经排除掉了节点为空的情况,那么剩下的就是左右节点不为空:
- 左右都不为空,比较节点数值,不相同就return false
此时左右节点不为空,且数值也不相同的情况我们也处理了。
代码如下:
if (left == null && right != null) {
return false;
}
if (left != null && right == null) {
return false;
}
if (left == null && right == null) {
return true;
}
if (left.val != right.val) {
return false;
}
注意上面最后一种情况,我没有使用else,而是else if, 因为我们把以上情况都排除之后,剩下的就是 左右节点都不为空,且数值相同的情况。
3.确定单层递归的逻辑
此时才进入单层递归的逻辑,单层递归的逻辑就是处理 左右节点都不为空,且数值相同的情况。
- 比较二叉树外侧是否对称:传入的是左节点的左孩子,右节点的右孩子。
- 比较内侧是否对称,传入左节点的右孩子,右节点的左孩子。
- 如果左右都对称就返回true ,有一侧不对称就返回false 。
代码如下:
// 比较外侧
boolean compareOutside = compare(left.left, right.right);
// 比较内侧
boolean compareInside = compare(left.right, right.left);
return compareOutside && compareInside;
如上代码中,我们可以看出使用的遍历方式,左子树左右中,右子树右左中,所以我把这个遍历顺序也称之为“后序遍历”(尽管不是严格的后序遍历)。
迭代法整体代码:
/**
* 递归法
*/
public boolean isSymmetric1(TreeNode root) {
return compare(root.left, root.right);
}
private boolean compare(TreeNode left, TreeNode right) {
if (left == null && right != null) {
return false;
}
if (left != null && right == null) {
return false;
}
if (left == null && right == null) {
return true;
}
if (left.val != right.val) {
return false;
}
// 比较外侧
boolean compareOutside = compare(left.left, right.right);
// 比较内侧
boolean compareInside = compare(left.right, right.left);
return compareOutside && compareInside;
}
注意终止条件:
if (left == null && right != null) {
return false;
}
if (left != null && right == null) {
return false;
}
if (left == null && right == null) {
return true;
}
if (left.val != right.val) {
return false;
}
迭代法
这道题目我们也可以使用迭代法,但要注意,这里的迭代法可不是前中后序的迭代写法,因为本题的本质是判断两个树是否是相互翻转的,其实已经不是所谓二叉树遍历的前中后序的关系了。
这里我们可以使用队列来比较两个树(根节点的左右子树)是否相互翻转,(注意这不是层序遍历)
使用队列
通过队列来判断根节点的左子树和右子树的内侧和外侧是否相等,如动画所示:
如下的条件判断和递归的逻辑是一样的。
代码如下
/**
* 迭代法
* 使用双端队列,相当于两个栈
*/
public boolean isSymmetric(TreeNode root) {
Deque<TreeNode> deque = new LinkedList<>();
deque.offerFirst(root.left);
deque.offerLast(root.right);
while (!deque.isEmpty()) {
TreeNode leftNode = deque.pollFirst();
TreeNode rightNode = deque.pollLast();
if (leftNode == null && rightNode == null) {
continue;
}
// if (leftNode == null && rightNode != null) {
// return false;
// }
// if (leftNode != null && rightNode == null) {
// return false;
// }
// if (leftNode.val != rightNode.val) {
// return false;
// }
// 以上三个判断条件合并
if (leftNode == null || rightNode == null || leftNode.val != rightNode.val) {
return false;
}
deque.offerFirst(leftNode.left);
deque.offerFirst(leftNode.right);
deque.offerLast(rightNode.right);
deque.offerLast(rightNode.left);
}
return true;
}
补充
为什么要用后序遍历
需要收集孩子信息向上一级返回---->后序遍历
ps:部分图片和代码来自代码随想录和Leetcode官网