二叉树的前中后序遍历(迭代法)
目录
题目链接:
题目:
解题思路:
代码:
前序遍历:
中序遍历:
后序遍历:
总结:
题目链接:
144. 二叉树的前序遍历 - 力扣(LeetCode)
94. 二叉树的中序遍历 - 力扣(LeetCode)
145. 二叉树的后序遍历 - 力扣(LeetCode)
题目:
给你二叉树的根节点 root,返回它节点值的 前序 遍历。
示例 1:
输入:root = [1,null,2,3]
输出:[1,2,3]
(解释:二叉树结构为根节点是 1,1 的右子节点是 2,2 的左子节点是 3,前序遍历顺序为根 - 左 - 右,所以遍历结果为 1、2、3)
二叉树的中序遍历
简单 相关标签 相关企业 Ax
给定一个二叉树的根节点 root,返回 它的 中序 遍历。
示例 1:
(图为二叉树结构:节点 1 的右子节点是 2,节点 2 的左子节点是 3)
输入: root = [1,null,2,3]
输出: [1,3,2]
示例 2:
输入: root = []
输出: []
二叉树的后序遍历
简单 相关标签 相关企业 Ax
给你一棵二叉树的根节点 root,返回其节点值的后序遍历。
示例 1:
输入: root = [1,null,2,3]
输出: [3,2,1]
解释:
(下方有对应二叉树结构示意图,根节点是 1,1 的右子节点是 2,2 的左子节点是 3)
解题思路:
使用栈和集合 即可,还有统一迭代法的形式(后续有)
前序是中左右,但是进栈是中右左
那同理可知后序是左右中,那进栈中左右,再reverse即可
代码:
前序遍历:
/*** 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<Integer> preorderTraversal(TreeNode root) {List<Integer> res=new ArrayList<>();if(root==null) return res;Stack<TreeNode> st=new Stack<>();st.push(root);while(!st.empty()){TreeNode head=st.pop();res.add(head.val);if(head.right!=null) st.push(head.right);if(head.left!=null) st.push(head.left);}return res;}
}
二叉树前序遍历的迭代实现:基于栈的深度解析
二叉树的前序遍历是树结构操作中的基础算法,其 "根→左→右" 的访问顺序在很多场景中都有广泛应用。除了直观的递归实现外,迭代方式通过手动维护栈结构来模拟遍历过程,能有效避免递归可能带来的栈溢出问题。本文将详细解析一个基于栈的二叉树前序遍历迭代实现,帮助读者深入理解其原理、执行流程及优势。
前序遍历的核心概念
前序遍历(Preorder Traversal)是二叉树的三种基本遍历方式之一,其核心规则是:对于任意节点,先访问该节点本身,再遍历其左子树,最后遍历其右子树,即 "根→左→右" 的访问顺序。每个节点被访问且仅被访问一次。
例如,对于二叉树 [1,null,2,3](根节点为 1,1 的右子节点为 2,2 的左子节点为 3),其前序遍历结果为 [1,2,3],这是按照节点 1→左子树(空)→右子树(节点 2→左子树节点 3→右子树(空))的顺序访问的结果。
迭代实现的核心思路
递归实现利用函数调用栈自然地维护了遍历顺序,而迭代实现则需要手动创建栈来模拟这一过程。前序遍历的迭代实现核心思路是:
使用栈存储待访问的节点
每次弹出栈顶节点并访问(加入结果列表)
按照 "右子节点→左子节点" 的顺序将弹出节点的子节点入栈(因为栈是后进先出结构,这样才能保证左子节点先被访问)
重复步骤 2-3,直到栈为空
这种方式通过栈的后进先出(LIFO)特性,完美模拟了前序遍历 "根→左→右" 的访问顺序。
完整代码实现
下面是二叉树前序遍历的迭代实现代码:
二叉树前序遍历的迭代实现
V1
创建时间:19:56
代码逐行深度解析
1. 二叉树节点定义
代码开头定义了二叉树节点的结构:
java
运行
/**
* 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;
* }
* }
*/
这个类定义了二叉树的基本构成单元,包含:
val:节点存储的整数 value
left:指向左子节点的引用
right:指向右子节点的引用
三个构造方法:分别用于创建不同状态的节点
所有二叉树操作都基于这个节点结构,理解它是掌握树遍历的基础。
2. 初始化结果列表
java
运行
List<Integer> res = new ArrayList<>();
创建一个 ArrayList 实例 res 用于存储遍历过程中收集的节点值。选择 ArrayList 是因为它在添加元素和随机访问时性能优良,适合存储有序的遍历结果。
3. 处理空树情况
java
运行
if (root == null) return res;
这是一个重要的边界条件处理。当输入的根节点为 null 时(表示空树),直接返回空的结果列表,避免后续不必要的操作。
4. 初始化栈并推入根节点
java
运行
Stack<TreeNode> st = new Stack<>();
st.push(root);
创建一个 Stack 实例 st,用于存储待访问的节点。栈的特性(后进先出)是实现前序遍历的关键。
将根节点 root 推入栈中,作为遍历的起始点。
5. 栈循环处理逻辑
java
运行
while (!st.empty()) {
// 弹出栈顶节点并访问
TreeNode head = st.pop();
res.add(head.val);
// 右子节点先入栈
if (head.right != null) {
st.push(head.right);
}
// 左子节点后入栈
if (head.left != null) {
st.push(head.left);
}
}
这个循环是迭代实现的核心,只要栈不为空,就持续处理节点,直到所有节点都被访问。循环内部包含三个关键步骤:
5.1 弹出栈顶节点并访问
java
运行
TreeNode head = st.pop();
res.add(head.val);
弹出栈顶节点 head,这是当前要访问的节点
将节点值 head.val 加入结果列表 res,完成对该节点的访问
这一步对应前序遍历中的 "根" 操作,即先访问当前节点。
5.2 右子节点入栈
java
运行
if (head.right != null) {
st.push(head.right);
}
将当前节点的右子节点推入栈中(如果存在)。注意这里右子节点先入栈,这是因为栈是后进先出的结构,后续左子节点入栈后会被先弹出访问,从而保证 "左→右" 的顺序。
5.3 左子节点入栈
java
运行
if (head.left != null) {
st.push(head.left);
}
将当前节点的左子节点推入栈中(如果存在)。由于左子节点后于右子节点入栈,根据栈的后进先出特性,左子节点会先被弹出访问,这就保证了前序遍历 "根→左→右" 的顺序。
6. 返回遍历结果
java
运行
return res;
当栈为空时,所有节点都已被访问,结果列表 res 中已按前序遍历顺序存储了所有节点值,将其返回即可。
算法执行过程演示
为了直观理解迭代实现的执行流程,我们以示例 root = [1,null,2,3] 为例进行详细演示:
该二叉树的结构如下:
plaintext
1
\
2
/
3
执行步骤分解:
初始化 res = [],检查 root 不为 null
创建栈 st,将 root(节点 1)入栈,st = [1]
进入循环(栈不为空):
弹出栈顶节点 1,res.add(1) → res = [1]
节点 1 的右子节点是 2,将 2 入栈 → st = [2]
节点 1 的左子节点是 null,不操作
栈现在为 [2]
继续循环(栈不为空):
弹出栈顶节点 2,res.add(2) → res = [1,2]
节点 2 的右子节点是 null,不操作
节点 2 的左子节点是 3,将 3 入栈 → st = [3]
栈现在为 [3]
继续循环(栈不为空):
弹出栈顶节点 3,res.add(3) → res = [1,2,3]
节点 3 的右子节点是 null,不操作
节点 3 的左子节点是 null,不操作
栈现在为空
循环结束,返回 res = [1,2,3],与预期结果一致
从执行过程可以清晰看到,通过 "根节点出栈访问→右子节点入栈→左子节点入栈" 的循环操作,完美实现了 "根→左→右" 的前序遍历顺序。栈的后进先出特性在这里起到了关键作用。
算法复杂度分析
时间复杂度
对于包含 n 个节点的二叉树,每个节点都会被推入栈一次并弹出栈一次,因此入栈和出栈操作的总次数为 O (n)。每个节点的值会被加入结果列表一次,也是 O (n) 操作。因此,整体时间复杂度为 O(n),其中 n 是二叉树的节点总数。
空间复杂度
空间复杂度主要来自两个方面:
栈的存储空间:在最坏情况下(二叉树退化为左斜树,即每个节点只有左子节点),栈需要存储所有节点,此时空间复杂度为 O(n);在平均情况下(平衡二叉树),栈的深度为 log (n),空间复杂度为 O(log n)。
结果列表:用于存储遍历结果的列表需要容纳所有 n 个节点的值,因此额外占用 O(n) 的空间。
综合来看,算法的整体空间复杂度为 O(n)。
迭代实现与递归实现的对比
前序遍历有递归和迭代两种主要实现方式,它们各有优缺点:
实现方式 核心思想 优点 缺点
递归实现 利用函数调用栈,按照 "根→左→右" 顺序递归访问 代码简洁直观,易于理解和编写 对于深度极大的树可能导致栈溢出;函数调用有一定性能开销
迭代实现 手动维护栈,模拟递归过程 避免栈溢出风险;性能开销较小 代码相对复杂;需要手动管理栈的操作
递归实现的代码通常更简洁:
java
运行
public List<Integer> preorderTraversal(TreeNode root) {
List<Integer> res = new ArrayList<>();
preorder(res, root);
return res;
}
private void preorder(List<Integer> res, TreeNode root) {
if (root == null) return;
res.add(root.val);
preorder(res, root.left);
preorder(res, root.right);
}
但迭代实现通过手动控制栈,避免了递归调用可能带来的栈溢出问题,在处理大型二叉树时更为稳健。
算法的优化与扩展
空间优化思考
当前实现的空间复杂度为 O (n),在某些场景下可以进一步优化。有一种 Morris 遍历算法可以实现 O (1) 空间复杂度的前序遍历,其核心思想是利用树的空指针建立临时链接,避免使用栈或递归。但 Morris 算法实现较为复杂,可读性较差,通常在对空间有严格要求的场景下使用。
扩展到其他遍历方式
基于栈的迭代思想可以扩展到中序和后序遍历,只需调整节点访问和入栈的顺序:
中序遍历:先将左子树全部入栈,弹出节点时访问,再处理右子树
后序遍历:可以通过两个栈实现,或通过标记节点是否已访问来实现
例如,后序遍历的迭代实现(双栈法):
java
运行
public List<Integer> postorderTraversal(TreeNode root) {
List<Integer> res = new ArrayList<>();
if (root == null) return res;
Stack<TreeNode> s1 = new Stack<>();
Stack<TreeNode> s2 = new Stack<>();
s1.push(root);
while (!s1.isEmpty()) {
TreeNode node = s1.pop();
s2.push(node);
if (node.left != null) s1.push(node.left);
if (node.right != null) s1.push(node.right);
}
while (!s2.isEmpty()) {
res.add(s2.pop().val);
}
return res;
}
实际应用场景
前序遍历的迭代实现在实际开发中有广泛应用:
树的序列化与反序列化:前序遍历常用于将树结构转换为线性序列(序列化),以及从序列重建树(反序列化)。
路径搜索:在从根节点到目标节点的路径搜索中,前序遍历可以及早发现目标节点,适合深度优先搜索(DFS)场景。
表达式树的前缀表达式生成:前序遍历表达式树可以得到前缀表达式(波兰式),这种表达式无需括号即可明确运算顺序,适合计算机求值。
语法分析:在编译器的语法分析阶段,前序遍历抽象语法树(AST)可用于生成中间代码。
文件系统遍历:文件系统通常组织为树结构,前序遍历可用于实现深度优先的文件搜索。
总结
本文详细解析了二叉树前序遍历的迭代实现,该实现通过手动维护栈结构,模拟了 "根→左→右" 的访问顺序。核心思路是:弹出栈顶节点并访问,然后按 "右子节点→左子节点" 的顺序将子节点入栈,利用栈的后进先出特性保证遍历顺序。
迭代实现的关键要点:
利用栈存储待访问的节点
弹出节点时立即访问(加入结果列表)
右子节点先入栈,左子节点后入栈,确保左子节点先被访问
循环处理直到栈为空
与递归实现相比,迭代实现避免了函数调用栈可能带来的栈溢出问题,在处理大型二叉树时更为稳健。理解这种实现方式不仅有助于掌握前序遍历算法,还能加深对栈数据结构和树遍历本质的理解。
在实际应用中,应根据具体场景选择合适的实现方式:对于代码简洁性要求高且树深度可控的场景,递归实现更合适;对于可能处理极深树的场景,迭代实现更为可靠。掌握前序遍历的迭代实现,对于深入学习数据结构和算法具有重要意义。
中序遍历:
/*** 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<Integer> inorderTraversal(TreeNode root) {List<Integer> res=new ArrayList<>();if(root==null){return res;}Stack<TreeNode> st=new Stack<>();while(root!=null||!st.empty()){if(root!=null){st.push(root);root=root.left;}else{root=st.pop();res.add(root.val);root=root.right;}}return res;}
}
二叉树中序遍历的迭代实现:基于栈的深度解析
二叉树的中序遍历是树结构操作中最常用的遍历方式之一,其 "左→根→右" 的访问顺序在二叉搜索树(BST)中具有特殊意义 —— 能得到一个递增的有序序列。除了递归实现外,迭代方式通过手动维护栈结构来模拟遍历过程,能有效避免递归可能带来的栈溢出问题。本文将详细解析一个基于栈的二叉树中序遍历迭代实现,帮助读者深入理解其原理、执行流程及优势。
中序遍历的核心概念
中序遍历(Inorder Traversal)是二叉树的三种基本遍历方式之一,其核心规则是:对于任意节点,先遍历其左子树,再访问该节点本身,最后遍历其右子树,即 "左→根→右" 的访问顺序。每个节点被访问且仅被访问一次。
例如,对于二叉树 [1,null,2,3](根节点为 1,1 的右子节点为 2,2 的左子节点为 3),其中序遍历结果为 [1,3,2],这是按照左子树(1 的左子树为空)→节点 1→右子树(2 的左子树 3→节点 2→2 的右子树为空)的顺序访问的结果。
迭代实现的核心思路
递归实现利用函数调用栈自然地维护了遍历顺序,而迭代实现则需要手动创建栈来模拟这一过程。中序遍历的迭代实现核心思路是:
使用栈存储待访问的节点
先将当前节点的所有左子节点依次入栈,直到没有左子节点为止
弹出栈顶节点并访问(加入结果列表)
将当前节点指向弹出节点的右子节点,重复步骤 2-3
当当前节点为 null 且栈为空时,遍历结束
这种方式通过栈的后进先出(LIFO)特性,完美模拟了中序遍历 "左→根→右" 的访问顺序。
完整代码实现
下面是二叉树中序遍历的迭代实现代码:
二叉树中序遍历的迭代实现
V1
创建时间:20:04
代码逐行深度解析
1. 二叉树节点定义
代码开头定义了二叉树节点的结构:
java
运行
/**
* 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;
* }
* }
*/
这个类定义了二叉树的基本构成单元,包含:
val:节点存储的整数 value
left:指向左子节点的引用
right:指向右子节点的引用
三个构造方法:分别用于创建不同状态的节点
所有二叉树操作都基于这个节点结构,理解它是掌握树遍历的基础。
2. 初始化结果列表
java
运行
List<Integer> res = new ArrayList<>();
创建一个 ArrayList 实例 res 用于存储遍历过程中收集的节点值。选择 ArrayList 是因为它在添加元素和随机访问时性能优良,适合存储有序的遍历结果。
3. 处理空树情况
java
运行
if (root == null) {
return res;
}
这是一个重要的边界条件处理。当输入的根节点为 null 时(表示空树),直接返回空的结果列表,避免后续不必要的操作。
4. 初始化栈
java
运行
Stack<TreeNode> st = new Stack<>();
创建一个 Stack 实例 st,用于存储待访问的节点。栈的特性(后进先出)是实现中序遍历的关键。与前序遍历不同,中序遍历的栈初始化时并不立即推入根节点,而是在循环中逐步处理。
5. 核心循环逻辑
java
运行
while (root != null || !st.empty()) {
// 左子树全部入栈
if (root != null) {
st.push(root);
root = root.left;
} else {
// 弹出栈顶节点并访问
root = st.pop();
res.add(root.val);
// 转向右子树
root = root.right;
}
}
这个循环是迭代实现的核心,循环条件是 " 当前节点不为空 或 栈不为空 ",确保所有节点都能被处理。循环内部包含两个主要分支:
5.1 左子树入栈分支
java
运行
if (root != null) {
st.push(root);
root = root.left;
}
当当前节点 root 不为空时,执行以下操作:
将当前节点 root 推入栈中(暂不访问,因为中序遍历需要先处理左子树)
将 root 指向其左子节点 root.left
这个过程会持续进行,直到 root 变为 null(即到达左子树的最左端)。这一步确保了我们先处理所有左子节点,符合中序遍历 "左" 的要求。
5.2 访问节点并处理右子树分支
java
运行
else {
// 弹出栈顶节点并访问
root = st.pop();
res.add(root.val);
// 转向右子树
root = root.right;
}
当当前节点 root 为 null 时(表示左子树已处理完毕),执行以下操作:
从栈中弹出栈顶节点,这是当前需要访问的节点(左子树已处理完毕)
将节点值 root.val 加入结果列表 res,完成对该节点的访问(符合中序遍历 "根" 的要求)
将 root 指向其右子节点 root.right,准备处理右子树(符合中序遍历 "右" 的要求)
6. 返回遍历结果
java
运行
return res;
当循环结束时(当前节点为 null 且栈为空),所有节点都已被访问,结果列表 res 中已按中序遍历顺序存储了所有节点值,将其返回即可。
算法执行过程演示
为了直观理解迭代实现的执行流程,我们以示例 root = [1,null,2,3] 为例进行详细演示:
该二叉树的结构如下:
plaintext
1
\
2
/
3
执行步骤分解:
执行左子树入栈分支:将 1 入栈 → st = [1],root 指向 1 的左子节点(null)
执行访问节点分支:弹出栈顶节点 1 → st = [],res.add(1) → res = [1]
root 指向 1 的右子节点(节点 2)
执行左子树入栈分支:将 2 入栈 → st = [2],root 指向 2 的左子节点(节点 3)
执行左子树入栈分支:将 3 入栈 → st = [2,3],root 指向 3 的左子节点(null)
执行访问节点分支:弹出栈顶节点 3 → st = [2],res.add(3) → res = [1,3]
root 指向 3 的右子节点(null)
执行访问节点分支:弹出栈顶节点 2 → st = [],res.add(2) → res = [1,3,2]
root 指向 2 的右子节点(null)
从执行过程可以清晰看到,通过 "左子树全部入栈→弹出节点访问→转向右子树" 的循环操作,完美实现了 "左→根→右" 的中序遍历顺序。栈在这里起到了暂存节点的作用,确保了节点按正确顺序被访问。
算法复杂度分析
时间复杂度
对于包含 n 个节点的二叉树,每个节点都会被推入栈一次并弹出栈一次,因此入栈和出栈操作的总次数为 O (n)。每个节点的值会被加入结果列表一次,也是 O (n) 操作。因此,整体时间复杂度为 O(n),其中 n 是二叉树的节点总数。
空间复杂度
空间复杂度主要来自两个方面:
综合来看,算法的整体空间复杂度为 O(n)。
迭代实现与递归实现的对比
中序遍历有递归和迭代两种主要实现方式,它们各有优缺点:
实现方式 核心思想 优点 缺点
递归实现 利用函数调用栈,按照 "左→根→右" 顺序递归访问 代码简洁直观,易于理解和编写 对于深度极大的树可能导致栈溢出;函数调用有一定性能开销
迭代实现 手动维护栈,模拟递归过程 避免栈溢出风险;性能开销较小 代码相对复杂;需要手动管理栈的操作
递归实现的代码通常更简洁:
java
运行
public List<Integer> inorderTraversal(TreeNode root) {
List<Integer> res = new ArrayList<>();
inorder(res, root);
return res;
}
private void inorder(List<Integer> res, TreeNode root) {
if (root == null) return;
inorder(res, root.left);
res.add(root.val);
inorder(res, root.right);
}
但迭代实现通过手动控制栈,避免了递归调用可能带来的栈溢出问题,在处理大型二叉树时更为稳健。
算法的优化与扩展
空间优化思考
当前实现的空间复杂度为 O (n),在某些场景下可以进一步优化。Morris 遍历算法可以实现 O (1) 空间复杂度的中序遍历,其核心思想是利用树的空指针建立临时链接(线索),避免使用栈或递归。但 Morris 算法实现较为复杂,可读性较差,通常在对空间有严格要求的场景下使用。
扩展到其他遍历方式
基于栈的迭代思想可以扩展到前序和后序遍历,只需调整节点访问和入栈的顺序:
前序遍历:访问节点后,先将右子节点入栈,再将左子节点入栈
后序遍历:可以通过两个栈实现,或通过标记节点是否已访问来实现
例如,前序遍历的迭代实现:
java
运行
public List<Integer> preorderTraversal(TreeNode root) {
List<Integer> res = new ArrayList<>();
if (root == null) return res;
Stack<TreeNode> st = new Stack<>();
st.push(root);
while (!st.empty()) {
TreeNode node = st.pop();
res.add(node.val);
if (node.right != null) st.push(node.right);
if (node.left != null) st.push(node.left);
}
return res;
}
实际应用场景
中序遍历的迭代实现在实际开发中有广泛应用:
总结
本文详细解析了二叉树中序遍历的迭代实现,该实现通过手动维护栈结构,模拟了 "左→根→右" 的访问顺序。核心思路是:先将所有左子节点入栈,弹出节点时访问,然后转向右子树,利用栈的后进先出特性保证遍历顺序。
迭代实现的关键要点:
循环条件为 "当前节点不为空或栈不为空"
左子树全部入栈后再开始访问节点
访问节点后转向其右子树继续处理
栈在这里起到暂存节点的作用,确保按正确顺序访问
与递归实现相比,迭代实现避免了函数调用栈可能带来的栈溢出问题,在处理大型二叉树时更为稳健。理解这种实现方式不仅有助于掌握中序遍历算法,还能加深对栈数据结构和树遍历本质的理解。
在实际应用中,应根据具体场景选择合适的实现方式:对于代码简洁性要求高且树深度可控的场景,递归实现更合适;对于可能处理极深树的场景,迭代实现更为可靠。掌握中序遍历的迭代实现,对于深入学习数据结构和算法具有重要意义。
return Collections.reverse(res); Line 34: error: incompatible types: void cannot be converted to List<Integer> return Collections.reverse(res); ^
这个错误的原因是 Collections.reverse(res) 方法的返回值类型是 void(没有返回值),而你的方法声明需要返回一个 List<Integer> 类型的结果,因此出现了 " 无法将 void 转换为 List<Integer>" 的类型不匹配错误。
Collections.reverse() 方法的作用是原地反转列表中的元素(直接修改原列表),而不是返回一个新的反转后的列表。所以正确的用法应该是先调用反转方法,再返回原列表:
java
运行
// 错误写法
return Collections.reverse(res);
// 正确写法
Collections.reverse(res); // 先原地反转列表
return res; // 再返回反转后的列表
例如,在后序遍历的迭代实现中,如果你想通过反转前序遍历结果来得到后序遍历结果,正确的代码应该是:
java
运行
public List<Integer> postorderTraversal(TreeNode root) {
List<Integer> res = new ArrayList<>();
if (root == null) return res;
Stack<TreeNode> stack = new Stack<>();
stack.push(root);
// 先进行类似前序遍历的操作(根→右→左)
while (!stack.isEmpty()) {
TreeNode node = stack.pop();
res.add(node.val);
if (node.left != null) stack.push(node.left); // 左子节点后入栈
if (node.right != null) stack.push(node.right); // 右子节点先入栈
}
// 反转结果得到后序遍历(左→右→根)
Collections.reverse(res);
return res; // 返回反转后的列表
}
总结:Collections.reverse() 是一个修改原对象的方法,没有返回值,因此不能直接作为返回语句使用,需要分开写。
后序遍历:
/*** 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<Integer> postorderTraversal(TreeNode root) {List<Integer> res=new ArrayList<>();if(root==null){return res;}Stack<TreeNode> st=new Stack<>();st.push(root);while(!st.empty()){TreeNode node=st.pop();res.add(node.val);if(node.left!=null){st.push(node.left);}if(node.right!=null){st.push(node.right);}}Collections.reverse(res);return res;}
}
二叉树后序遍历的迭代实现:基于栈和反转的深度解析
二叉树的后序遍历是树结构操作中的重要遍历方式,其 "左→右→根" 的访问顺序在删除操作、表达式求值等场景中具有特殊价值。后序遍历的迭代实现相对复杂,但有一种巧妙的方法可以通过修改前序遍历并反转结果来高效实现。本文将详细解析这种基于栈和反转的后序遍历迭代实现,帮助读者深入理解其原理、执行流程及优势。
后序遍历的核心概念
后序遍历(Postorder Traversal)是二叉树的三种基本遍历方式之一,其核心规则是:对于任意节点,先遍历其左子树,再遍历其右子树,最后访问该节点本身,即 "左→右→根" 的访问顺序。每个节点被访问且仅被访问一次。
例如,对于二叉树 [1,null,2,3](根节点为 1,1 的右子节点为 2,2 的左子节点为 3),其后序遍历结果为 [3,2,1],这是按照左子树(3)→右子树(无)→节点 2→右子树(已处理)→节点 1 的顺序访问的结果。
基于反转的实现思路
后序遍历的直接迭代实现需要判断节点的访问状态,逻辑较为复杂。而一种巧妙的替代方案是:
执行一种 modified 前序遍历,访问顺序为 "根→右→左"
将遍历结果反转,得到的就是 "左→右→根" 的后序遍历结果
这种方法的原理是:
标准前序遍历:根→左→右
modified 前序遍历(交换左右子节点入栈顺序):根→右→左
反转 modified 前序遍历结果:左→右→根(即后序遍历)
这种思路将复杂的后序遍历转化为相对简单的前序遍历加反转操作,大大简化了代码实现。
完整代码实现
下面是基于这种思路的二叉树后序遍历迭代实现代码:
二叉树后序遍历的迭代实现(基于栈和反转)
V1
创建时间:20:24
代码逐行深度解析
1. 二叉树节点定义
代码开头定义了二叉树节点的结构:
java
运行
/**
* 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;
* }
* }
*/
这个类定义了二叉树的基本构成单元,包含:
val:节点存储的整数 value
left:指向左子节点的引用
right:指向右子节点的引用
三个构造方法:分别用于创建不同状态的节点
所有二叉树操作都基于这个节点结构,理解它是掌握树遍历的基础。
2. 初始化结果列表
java
运行
List<Integer> res = new ArrayList<>();
创建一个 ArrayList 实例 res 用于存储遍历过程中收集的节点值。选择 ArrayList 是因为它在添加元素和支持反转操作时性能优良。
3. 处理空树情况
java
运行
if (root == null) {
return res;
}
这是一个重要的边界条件处理。当输入的根节点为 null 时(表示空树),直接返回空的结果列表,避免后续不必要的操作。
4. 初始化栈并推入根节点
java
运行
Stack<TreeNode> st = new Stack<>();
st.push(root);
创建一个 Stack 实例 st,用于存储待访问的节点。栈的后进先出特性是实现 modified 前序遍历的关键。
将根节点 root 推入栈中,作为遍历的起始点。
5. 栈循环处理逻辑(modified 前序遍历)
java
运行
while (!st.empty()) {
// 弹出栈顶节点并访问
TreeNode node = st.pop();
res.add(node.val);
// 左子节点先入栈
if (node.left != null) {
st.push(node.left);
}
// 右子节点后入栈
if (node.right != null) {
st.push(node.right);
}
}
这个循环实现了 "根→右→左" 的 modified 前序遍历,只要栈不为空,就持续处理节点,直到所有节点都被访问。循环内部包含三个关键步骤:
5.1 弹出栈顶节点并访问
java
运行
TreeNode node = st.pop();
res.add(node.val);
弹出栈顶节点 node,这是当前要访问的节点
将节点值 node.val 加入结果列表 res,完成对该节点的访问
这一步对应 modified 前序遍历中的 "根" 操作,即先访问当前节点。
5.2 左子节点入栈
java
运行
if (node.left != null) {
st.push(node.left);
}
将当前节点的左子节点推入栈中(如果存在)。注意这里左子节点先入栈,这与标准前序遍历的入栈顺序相反。
5.3 右子节点入栈
java
运行
if (node.right != null) {
st.push(node.right);
}
将当前节点的右子节点推入栈中(如果存在)。由于右子节点后于左子节点入栈,根据栈的后进先出特性,右子节点会先被弹出访问,这就保证了 modified 前序遍历 "根→右→左" 的顺序。
6. 反转结果列表
java
运行
Collections.reverse(res);
调用 Collections.reverse() 方法对结果列表进行原地反转。这一步是将 modified 前序遍历的 "根→右→左" 结果转换为后序遍历的 "左→右→根" 结果的关键。
需要注意的是,Collections.reverse() 方法的返回值为 void(无返回值),它直接修改原列表,因此不能直接将其作为返回值使用(如错误写法 return Collections.reverse(res);)。
7. 返回遍历结果
java
运行
return res;
当反转完成后,结果列表 res 中已按后序遍历顺序存储了所有节点值,将其返回即可。
算法执行过程演示
为了直观理解这种实现的执行流程,我们以示例 root = [1,null,2,3] 为例进行详细演示:
该二叉树的结构如下:
plaintext
1
\
2
/
3
执行步骤分解:
初始化 res = [],检查 root(节点 1)不为 null
创建栈 st,将 root(节点 1)入栈,st = [1]
进入循环(栈不为空):
弹出栈顶节点 1,res.add(1) → res = [1]
节点 1 的左子节点是 null,不操作
节点 1 的右子节点是 2,将 2 入栈 → st = [2]
栈现在为 [2]
继续循环(栈不为空):
弹出栈顶节点 2,res.add(2) → res = [1,2]
节点 2 的左子节点是 3,将 3 入栈 → st = [3]
节点 2 的右子节点是 null,不操作
栈现在为 [3]
继续循环(栈不为空):
弹出栈顶节点 3,res.add(3) → res = [1,2,3]
节点 3 的左子节点是 null,不操作
节点 3 的右子节点是 null,不操作
栈现在为空
循环结束,执行 Collections.reverse(res) → res = [3,2,1]
返回 res = [3,2,1],与预期结果一致
从执行过程可以清晰看到:
modified 前序遍历得到 [1,2,3](根→右→左顺序)
反转后得到 [3,2,1](左→右→根顺序),即后序遍历结果
这种通过修改前序遍历顺序并反转结果的方法,巧妙地实现了后序遍历,大大简化了代码逻辑。
算法复杂度分析
时间复杂度
对于包含 n 个节点的二叉树:
每个节点会被推入栈一次并弹出栈一次,入栈和出栈操作的总次数为 O (n)
每个节点的值会被加入结果列表一次,是 O (n) 操作
Collections.reverse() 方法需要遍历列表一次,是 O (n) 操作
因此,整体时间复杂度为 O(n),其中 n 是二叉树的节点总数。
空间复杂度
空间复杂度主要来自三个方面:
栈的存储空间:在最坏情况下(二叉树退化为右斜树,即每个节点只有右子节点),栈需要存储所有节点,此时空间复杂度为 O(n);在平均情况下(平衡二叉树),栈的深度为 log (n),空间复杂度为 O(log n)。
结果列表:用于存储遍历结果的列表需要容纳所有 n 个节点的值,因此额外占用 O(n) 的空间。
反转操作:Collections.reverse() 方法是原地反转,不需要额外的空间。
综合来看,算法的整体空间复杂度为 O(n)。
与其他后序遍历实现的对比
后序遍历有多种迭代实现方式,主要包括:
实现方式 核心思想 优点 缺点
基于反转的实现 修改前序遍历为 "根→右→左",再反转结果 代码简洁,易于理解和实现 需要额外的反转操作;修改原列表
单栈标记法 使用栈存储节点,通过标记判断节点是否已访问 无需反转,直接得到结果 逻辑复杂,需要额外标记
双栈法 使用两个栈,第一个栈按 "根→左→右" 入栈,第二个栈存储弹出元素 思路清晰,无需标记 需要额外的栈空间
单栈标记法的示例代码:
java
运行
public List<Integer> postorderTraversal(TreeNode root) {
List<Integer> res = new ArrayList<>();
if (root == null) return res;
Stack<Object[]> stack = new Stack<>();
stack.push(new Object[]{root, false});
while (!stack.isEmpty()) {
Object[] entry = stack.pop();
TreeNode node = (TreeNode) entry[0];
boolean visited = (boolean) entry[1];
if (visited) {
res.add(node.val);
} else {
stack.push(new Object[]{node, true});
if (node.right != null) stack.push(new Object[]{node.right, false});
if (node.left != null) stack.push(new Object[]{node.left, false});
}
}
return res;
}
相比之下,基于反转的实现代码最为简洁,易于理解和记忆,是实际开发中常用的后序遍历实现方式。
实际应用场景
后序遍历的这种实现方式在实际开发中有广泛应用:
二叉树的删除操作:删除节点时需要先删除左右子节点,再删除当前节点,与后序遍历顺序一致。
计算二叉树的深度:需要先计算左右子树深度,再取最大值加 1,适合后序遍历。
表达式树求值:后序遍历表达式树可以得到后缀表达式(逆波兰式),适合计算机直接求值。
路径总和问题:需要遍历到叶子节点后再计算路径总和,后序遍历提供了天然的回溯机制。
资源释放:在释放树形结构的资源时,需要先释放子节点资源,再释放父节点资源,与后序遍历顺序一致。
总结
本文详细解析了二叉树后序遍历的一种巧妙迭代实现,该实现通过以下步骤完成:
执行 "根→右→左" 的 modified 前序遍历(通过调整左右子节点的入栈顺序实现)
反转遍历结果,得到 "左→右→根" 的后序遍历结果
这种方法的核心优势在于将复杂的后序遍历转化为相对简单的前序遍历加反转操作,大大简化了代码实现。代码的关键要点包括:
左子节点先入栈,右子节点后入栈,确保弹出顺序为 "根→右→左"
使用 Collections.reverse() 方法对结果进行原地反转
注意 Collections.reverse() 无返回值,需先调用再返回原列表
与其他后序遍历实现相比,这种方法代码简洁、易于理解和记忆,是实际开发中推荐的实现方式。理解这种实现不仅有助于掌握后序遍历算法,还能培养对不同遍历方式之间联系的洞察力,为解决更复杂的树结构问题奠定基础。
在实际应用中,这种实现方式特别适合对代码简洁性要求较高,且树的规模在可接受范围内的场景。掌握它对于深入学习数据结构和算法具有重要意义。
总结:
本文系统介绍了二叉树前序、中序和后序遍历的迭代实现方法。前序遍历采用栈结构,按;根→右→左 ;顺序入栈;中序遍历先全部左子树入栈再访问节点;后序遍历则通过修改前序遍历顺序为"根→右→左 ;后反转结果。三种遍历时间复杂度均为O(n),空间复杂度最坏O(n)。与递归实现相比,迭代方法避免了栈溢出风险,处理大型树更稳定。每种遍历方式都配有详细代码解析、执行示例和复杂度分析,并讨论了实际应用场景,为掌握树结构操作提供了系统指导。