题目信息 二叉树前中后序遍历
-
题目链接: 144. 二叉树的前序遍历
-
题目描述: 给你二叉树的根节点
root
,返回它节点值的 前序 遍历。 -
- 题目链接: 145. 二叉树的后序遍历
-
题目描述:给你一棵二叉树的根节点
root
,返回其节点值的 后序遍历 。 -
- 题目链接: 94. 二叉树的中序遍历
-
题目描述: 给定一个二叉树的根节点
root
,返回 它的 中序 遍历 。
解法一: {{二叉树的递归遍历}}
解题思路
每次写递归,都按照这三要素来写!
-
确定递归函数的参数和返回值: 确定哪些参数是递归的过程中需要处理的,那么就在递归函数里加上这个参数, 并且还要明确每次递归的返回值是什么进而确定递归函数的返回类型。
-
确定终止条件: 写完了递归算法, 运行的时候,经常会遇到栈溢出的错误,就是没写终止条件或者终止条件写的不对,操作系统也是用一个栈的结构来保存每一层递归的信息,如果递归没有终止,操作系统的内存栈必然就会溢出。
-
确定单层递归的逻辑: 确定每一层递归需要处理的信息。在这里也就会重复调用自己来实现递归的过程。
以下以前序遍历为例: -
确定递归函数的参数和返回值:因为要打印出前序遍历节点的数值,所以参数里需要传入 List<> result 来放节点的数值,除了这一点就不需要再处理什么数据了也不需要有返回值,所以递归函数返回类型就是void,代码如下:
void preorder(TreeNode root,List<Integer> result)
- 确定终止条件:在递归的过程中,如何算是递归结束了呢,当然是当前遍历的节点是空了,那么本层递归就要结束了,所以如果当前遍历的这个节点是空,就直接return,代码如下:
if (cur == NULL) return;
- 确定单层递归的逻辑:前序遍历是中左右的顺序,所以在单层递归的逻辑,是要先取中节点的数值,代码如下:
result.add(root.val); // 中
preorder(root.left,result); // 左
preorder(root.right,result); // 右
单层递归的逻辑就是按照中左右的顺序来处理的,这样二叉树的前序遍历,基本就写完了,再看一下完整代码:
代码实现
// 前序遍历·递归·LC144_二叉树的前序遍历
public List<Integer> preorderTraversal(TreeNode root) {
List<Integer> result = new ArrayList<>();
preorder(root,result);
return result;
}
void preorder(TreeNode root,List<Integer> result){
if (root == null){
return;
}
result.add(root.val);
preorder(root.left,result);
preorder(root.right,result);
}
// 中序遍历·递归·LC94_二叉树的中序遍历
public List<Integer> inorderTraversal(TreeNode root) {
List<Integer> result = new ArrayList<>();
inorder(root,result);
return result;
}
void inorder(TreeNode root,List<Integer> result){
if (root == null){
return;
}
inorder(root.left,result);
result.add(root.val);
inorder(root.right,result);
}
// 后序遍历·递归·LC145_二叉树的后序遍历
public List<Integer> postorderTraversal(TreeNode root){
List<Integer> result = new ArrayList<>();
postorder(root,result);
return result;
}
void postorder(TreeNode root,List<Integer> result){
if (root == null){
return;
}
postorder(root.left,result);
postorder(root.right,result);
result.add(root.val);
}
解法二: {{二叉树的迭代遍历}}
解题思路
前序遍历:
前序遍历是中左右,每次先处理的是中间节点,那么先将根节点放入栈中,然后将右孩子加入栈,再加入左孩子。
为什么要先加入 右孩子,再加入左孩子呢? 因为这样出栈的时候才是中左右的顺序。
动画如下:
// 前序遍历·迭代·LC144_二叉树的前序遍历
// 前序遍历顺序:中-左-右,入栈顺序:中-右-左
public List<Integer> preorderTraversal1(TreeNode root){
List<Integer> result = new ArrayList<>();
if (root == null){
return result;
}
Stack<TreeNode> stack = new Stack<>();
stack.push(root);
while (!stack.isEmpty()){
TreeNode node = stack.pop();
result.add(node.val);
if (node.right != null){
stack.push(node.right);
}
if (node.left != null){
stack.push(node.left);
}
}
return result;
}
此时会发现貌似使用迭代法写出前序遍历并不难,确实不难。
此时是不是想改一点前序遍历代码顺序就把中序遍历搞出来了?
其实还真不行!
但接下来,**再用迭代法写中序遍历的时候,会发现套路又不一样了,目前的前序遍历的逻辑无法直接应用到中序遍历上
中序遍历(迭代法)
为了解释清楚,我说明一下 刚刚在迭代的过程中,其实我们有两个操作:
- 处理:将元素放进result数组中
- 访问:遍历节点
分析一下为什么刚刚写的前序遍历的代码,不能和中序遍历通用呢,因为前序遍历的顺序是中左右,先访问的元素是中间节点,要处理的元素也是中间节点,所以刚刚才能写出相对简洁的代码,因为要访问的元素和要处理的元素顺序是一致的,都是中间节点。
那么再看看中序遍历,中序遍历是左中右,先访问的是二叉树顶部的节点,然后一层一层向下访问,直到到达树左面的最底部,再开始处理节点(也就是在把节点的数值放进result数组中),这就造成了处理顺序和访问顺序是不一致的。
那么在使用迭代法写中序遍历,就需要借用指针的遍历来帮助访问节点,栈则用来处理节点上的元素。
动画如下:
// 中序遍历·迭代·LC94_二叉树的中序遍历
// 中序遍历顺序: 左-中-右 入栈顺序: 左-右
public List<Integer> inorderTraversal1(TreeNode root) {
List<Integer> result = new ArrayList<>();
if (root == null){
return result;
}
Stack<TreeNode> stack = new Stack<>();
TreeNode cur = root;
while (cur != null || !stack.isEmpty()){
if (cur != null){
stack.push(cur);
cur = cur.left;
}else {
cur = stack.pop();
result.add(cur.val);
cur = cur.right;
}
}
return result;
}
代码实现
// 前序遍历·迭代·LC144_二叉树的前序遍历
// 前序遍历顺序:中-左-右,入栈顺序:中-右-左
public List<Integer> preorderTraversal1(TreeNode root){
List<Integer> result = new ArrayList<>();
if (root == null){
return result;
}
Stack<TreeNode> stack = new Stack<>();
stack.push(root);
while (!stack.isEmpty()){
TreeNode node = stack.pop();
result.add(node.val);
if (node.right != null){
stack.push(node.right);
}
if (node.left != null){
stack.push(node.left);
}
}
return result;
}
// 后序遍历·递归·LC145_二叉树的后序遍历
// 后序遍历顺序 左-右-中 入栈顺序:中-左-右 出栈顺序:中-右-左, 最后翻转结果
public List<Integer> postorderTraversal1(TreeNode root){
List<Integer> result = new ArrayList<>();
if (root == null){
return result;
}
Stack<TreeNode> stack = new Stack<>();
stack.push(root);
while (!stack.isEmpty()){
TreeNode node = stack.pop();
result.add(node.val);
if (node.left != null){
stack.push(node.left);
}
if (node.right != null){
stack.push(node.right);
}
}
Collections.reverse(result);
return result;
}
// 中序遍历·迭代·LC94_二叉树的中序遍历
// 中序遍历顺序: 左-中-右 入栈顺序: 左-右
public List<Integer> inorderTraversal1(TreeNode root) {
List<Integer> result = new ArrayList<>();
if (root == null){
return result;
}
Stack<TreeNode> stack = new Stack<>();
TreeNode cur = root;
while (cur != null || !stack.isEmpty()){
if (cur != null){
stack.push(cur);
cur = cur.left;
}else {
cur = stack.pop();
result.add(cur.val);
cur = cur.right;
}
}
return result;
}
解法三: {{二叉树的统一迭代法}}
解题思路
我们发现迭代法实现的先中后序,其实风格也不是那么统一,除了先序和后序,有关联,中序完全就是另一个风格了,一会用栈遍历,一会又用指针来遍历。
使用迭代法实现先中后序遍历,很难写出统一的代码,不像是递归法,实现了其中的一种遍历方式,其他两种只要稍稍改一下节点顺序就可以了。
其实针对三种遍历方式,使用迭代法是可以写出统一风格的代码!
重头戏来了,接下来介绍一下统一写法。
我们以中序遍历为例,使用栈的话,无法同时解决访问节点(遍历节点)和处理节点(将元素放进结果集)不一致的情况。
那我们就将访问的节点放入栈中,把要处理的节点也放入栈中但是要做标记。
如何标记呢,就是要处理的节点放入栈之后,紧接着放入一个空指针作为标记。 这种方法也可以叫做标记法。
中序遍历:
动画中,result数组就是最终结果集。
可以看出我们将访问的节点直接加入到栈中,但如果是处理的节点则后面放入一个空节点, 这样只有空节点弹出的时候,才将下一个节点放进结果集。
此时我们再来看前序遍历代码。
代码实现
public List<Integer> inorderTraversal(TreeNode root) {
List<Integer> result = new LinkedList<>();
Stack<TreeNode> st = new Stack<>();
if (root != null) st.push(root);
while (!st.empty()) {
TreeNode node = st.peek();
if (node != null) {
st.pop(); // 将该节点弹出,避免重复操作,下面再将右中左节点添加到栈中
if (node.right!=null) st.push(node.right); // 添加右节点(空节点不入栈)
st.push(node); // 添加中节点
st.push(null); // 中节点访问过,但是还没有处理,加入空节点做为标记。
if (node.left!=null) st.push(node.left); // 添加左节点(空节点不入栈)
} else { // 只有遇到空节点的时候,才将下一个节点放进结果集
st.pop(); // 将空节点弹出
node = st.peek(); // 重新取出栈中元素
st.pop();
result.add(node.val); // 加入到结果集
}
}
return result;
}
前序遍历
迭代法前序遍历代码如下: (注意此时我们和中序遍历相比仅仅改变了两行代码的顺序)
public List<Integer> preorderTraversal(TreeNode root) {
List<Integer> result = new LinkedList<>();
Stack<TreeNode> st = new Stack<>();
if (root != null) st.push(root);
while (!st.empty()) {
TreeNode node = st.peek();
if (node != null) {
st.pop(); // 将该节点弹出,避免重复操作,下面再将右中左节点添加到栈中
if (node.right!=null) st.push(node.right); // 添加右节点(空节点不入栈)
if (node.left!=null) st.push(node.left); // 添加左节点(空节点不入栈)
st.push(node); // 添加中节点
st.push(null); // 中节点访问过,但是还没有处理,加入空节点做为标记。
} else { // 只有遇到空节点的时候,才将下一个节点放进结果集
st.pop(); // 将空节点弹出
node = st.peek(); // 重新取出栈中元素
st.pop();
result.add(node.val); // 加入到结果集
}
}
return result;
}
迭代法后序遍历
后续遍历代码如下: (注意此时我们和中序遍历相比仅仅改变了两行代码的顺序)
代码实现
public List<Integer> postorderTraversal(TreeNode root) {
List<Integer> result = new LinkedList<>();
Stack<TreeNode> st = new Stack<>();
if (root != null) st.push(root);
while (!st.empty()) {
TreeNode node = st.peek();
if (node != null) {
st.pop(); // 将该节点弹出,避免重复操作,下面再将右中左节点添加到栈中
st.push(node); // 添加中节点
st.push(null); // 中节点访问过,但是还没有处理,加入空节点做为标记。
if (node.right!=null) st.push(node.right); // 添加右节点(空节点不入栈)
if (node.left!=null) st.push(node.left); // 添加左节点(空节点不入栈)
} else { // 只有遇到空节点的时候,才将下一个节点放进结果集
st.pop(); // 将空节点弹出
node = st.peek(); // 重新取出栈中元素
st.pop();
result.add(node.val); // 加入到结果集
}
}
return result;
}
主要区别:
- 前序遍历(根->左->右):
if (node.right!=null) st.push(node.right);
if (node.left!=null) st.push(node.left);
st.push(node);
st.push(null);
- 先将右子节点入栈,然后左子节点,最后是当前节点。
- 这确保了出栈顺序为:中、左、右。
2. 中序遍历(左->根->右):
```java
if (node.right!=null) st.push(node.right);
st.push(node);
st.push(null);
if (node.left!=null) st.push(node.left);
- 先将右子节点入栈,然后是当前节点,最后是左子节点。
- 这确保了出栈顺序为:左、中、右。
- 后序遍历(左->右->根):
st.push(node);
st.push(null);
if (node.right!=null) st.push(node.right);
if (node.left!=null) st.push(node.left);
- 先将当前节点入栈,然后是右子节点,最后是左子节点。
- 这确保了出栈顺序为:左、右、中。
详细解释:
1. 前序遍历:
- 最先将根节点的值加入结果集。
- 然后处理左子树,最后处理右子树。
- 入栈顺序(右->左->中)保证了处理顺序是(中->左->右)。
2. 中序遍历:
- 最先处理左子树。
- 然后将当前节点的值加入结果集。
- 最后处理右子树。
- 入栈顺序(右->中->左)保证了处理顺序是(左->中->右)。
3. 后序遍历:
- 最先处理左子树,然后处理右子树。
- 最后将当前节点的值加入结果集。
- 入栈顺序(中->右->左)保证了处理顺序是(左->右->中)
## 二叉树层序遍历
- 题目链接: [102. 二叉树的层序遍历](https://leetcode.cn/problems/binary-tree-level-order-traversal/)
- 题目描述: 给你二叉树的根节点 `root` ,返回其节点值的 **层序遍历** 。 (即逐层地,从左到右访问所有节点)。
## 解法一: {{迭代方式--借助队列}}
### 解题思路
层序遍历一个二叉树。就是从左到右一层一层的去遍历二叉树。这种遍历的方式和我们之前讲过的都不太一样。
需要借用一个辅助数据结构即队列来实现,**队列先进先出,符合一层一层遍历的逻辑,而用栈先进后出适合模拟深度优先遍历也就是递归的逻辑。**
**而这种层序遍历方式就是图论中的广度优先遍历,只不过我们应用在二叉树上。**
使用队列实现二叉树广度优先遍历,动画如下:
![102二叉树的层序遍历](https://code-thinking.cdn.bcebos.com/gifs/102%E4%BA%8C%E5%8F%89%E6%A0%91%E7%9A%84%E5%B1%82%E5%BA%8F%E9%81%8D%E5%8E%86.gif)
### 代码实现
```java
public List<List<Integer>> resList = new ArrayList<List<Integer>>();
public List<List<Integer>> levelOrder(TreeNode root) {
// List<List<Integer>> resList = new ArrayList<List<Integer>>();
checkFun02(root);
return resList;
}
public void checkFun02(TreeNode node){
if (node == null){
return;
}
Queue<TreeNode> que = new LinkedList<TreeNode>();
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);
}
}
- 队列的基本概念:
Queue que = new LinkedList();
- 队列是一种先进先出(FIFO)的数据结构。
- 在Java中,Queue是一个接口,LinkedList是其常用实现之一。
- 这里创建了一个存储TreeNode对象的队列。
- 队列的主要操作: a. 入队(Enqueue):
```java
que.offer(node);
- `offer`方法用于将元素添加到队列尾部。
- 如果成功添加返回true,如果队列已满返回false。
b. 出队(Dequeue):
```java
TreeNode tmpNode = que.poll();
- `poll`方法用于移除并返回队列头部的元素。
- 如果队列为空,返回null。
c. 检查队列是否为空:
```java
while (!que.isEmpty()) {
// ...
}
- `isEmpty`方法检查队列是否为空。
d. 获取队列大小:
```java
int len = que.size();
- `size`方法返回队列中的元素数量。
- 队列在层序遍历中的应用:
- 初始化:将根节点入队。
- 逐层处理:
- 当队列不为空时,获取当前队列的大小(即当前层的节点数)。
- 依次处理这一层的所有节点:
- 出队一个节点。
- 处理这个节点(这里是将值加入结果列表)。
- 如果该节点有左右子节点,将它们入队。
- 这个过程保证了按层次顺序处理节点。
## 解法二: {{递归方式}}
### 解题思路
1. 方法:
```java
public void checkFun01(TreeNode node, Integer deep)
```
- `node`: 当前处理的树节点
- `deep`: 当前节点的深度(层级)
2. 基本情况处理:
```java
if (node == null) return;
```
- 如果当前节点为空,直接返回,结束这个分支的递归。
3. 深度增加:
```java
deep++;
```
- 每深入一层,深度加1。
4. 确保结果列表有足够的层:
```java
if (resList.size() < deep) {
List<Integer> item = new ArrayList<Integer>();
resList.add(item);
}
```
- 如果结果列表的大小小于当前深度,说明遇到了新的一层。
- 创建一个新的ArrayList来存储这一层的节点值。
- 将新创建的列表添加到结果列表中。
5. 添加当前节点值到对应层:
```java
resList.get(deep - 1).add(node.val);
```
- 将当前节点的值添加到对应深度的列表中。
- 注意这里使用 `deep - 1` 是因为列表索引从0开始,而深度从1开始。
6. 递归处理左右子节点:
```java
checkFun01(node.left, deep);
checkFun01(node.right, deep);
```
- 递归调用,分别处理左子节点和右子节点。
- 传递当前的深度值。
### 代码实现
```java
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);
}
```