LeetCode 257. 二叉树所有路径的迭代解法:栈的妙用与类型设计深度解析
一、题目背景与核心需求
题目描述
LeetCode 257. 二叉树的所有路径要求我们返回从根节点到所有叶子节点的路径。例如,对于二叉树:
1/ \2 3/
4
应返回 ["1->2->4", "1->3"]
。这类问题本质上是要求我们完成深度优先遍历(DFS),并在遍历过程中记录路径信息。
核心难点
- 路径动态构建:如何在遍历过程中实时记录从根到当前节点的路径。
- 遍历顺序控制:确保每条路径都是从根到叶子的完整路径。
- 非递归实现:使用栈结构模拟递归过程,避免递归可能带来的栈溢出问题。
二、迭代解法的核心设计:栈的双元素存储策略
代码实现
/*** 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<String> binaryTreePaths(TreeNode root) {List<String> res = new ArrayList<>();if (root == null) {return res;}Stack<Object> path = new Stack<>();path.push(root);path.push(root.val + "");while (!path.empty()) {String tempPath = (String) path.pop();TreeNode node = (TreeNode) path.pop();if (node.left == null && node.right == null) {res.add(tempPath);}if (node.right != null) {path.push(node.right);path.push(tempPath + "->" + node.right.val);}if (node.left != null) {path.push(node.left);path.push(tempPath + "->" + node.left.val);}}return res;}
}
核心设计解析:为什么选择Object类型的栈?
1. 栈的双重角色:节点与路径的绑定存储
-
该解法的核心创新在于使用一个栈同时存储两种类型的数据:
TreeNode
类型:记录当前遍历的节点位置。String
类型:记录从根到当前节点的路径字符串。
-
Object类型的必要性:
Java的泛型栈通常只能存储单一类型,但本题需要同时存储TreeNode
和String
,因此选择Stack<Object>
作为容器,利用Java的自动装箱和拆箱机制实现多类型存储。
2. Object类型的优缺点
-
优点:
灵活应对多类型数据存储,用一个栈完成节点遍历和路径记录,代码简洁高效。 -
缺点:
需要显式类型转换(如(String)
和(TreeNode)
),若入栈出栈顺序错误会导致类型转换异常,对编程逻辑的严谨性要求更高。
三、出入栈逻辑深度解析:LIFO特性的巧妙应用
1. 初始入栈:根节点与初始路径
path.push(root);
path.push(root.val + "");
- 入栈顺序:先压入根节点,再压入根节点值的字符串表示。
- 栈内状态:
[root, "1"]
(假设根节点值为1)。
2. 出栈处理:先路径后节点的顺序
String tempPath = (String) path.pop();
TreeNode node = (TreeNode) path.pop();
- 出栈顺序与入栈顺序相反(LIFO),先取出路径字符串,再取出节点。
- 这种设计确保了每次处理节点时,路径字符串已正确记录到该节点的路径。
3. 子节点入栈:先右后左的策略
if (node.right != null) {path.push(node.right);path.push(tempPath + "->" + node.right.val);
}
if (node.left != null) {path.push(node.left);path.push(tempPath + "->" + node.left.val);
}
- 入栈顺序分析:先压入右子节点,再压入左子节点。
- LIFO特性的应用:由于栈的后进先出特性,左子节点会先于右子节点出栈,从而实现先左后右的深度优先遍历顺序。
4. 栈操作模拟:以示例二叉树为例
示例二叉树:
1/ \2 3/
4
栈操作流程:
-
初始状态:
栈:[1, "1"]
-
第一次循环:
- 出栈:
"1"
,1
- 检查是否为叶子节点:否
- 入栈右子节点3:
[3, "1->3"]
- 入栈左子节点2:
[2, "1->2"]
栈状态:[3, "1->3", 2, "1->2"]
- 出栈:
-
第二次循环:
- 出栈:
"1->2"
,2
- 检查是否为叶子节点:否
- 入栈右子节点(无),入栈左子节点4:
[4, "1->2->4"]
栈状态:[3, "1->3", 4, "1->2->4"]
- 出栈:
-
第三次循环:
- 出栈:
"1->2->4"
,4
- 检查是否为叶子节点:是,加入结果
res
栈状态:[3, "1->3"]
- 出栈:
-
第四次循环:
- 出栈:
"1->3"
,3
- 检查是否为叶子节点:是,加入结果
res
栈状态:空
- 出栈:
-
最终结果:
["1->2->4", "1->3"]
四、栈结构与递归的等价性分析
1. 迭代栈与递归栈的映射关系
递归实现 | 迭代栈实现 |
---|---|
递归调用栈自动保存上下文 | 手动维护栈保存节点和路径 |
递归返回时自动回溯 | 栈出栈时自然回溯 |
代码简洁但可能栈溢出 | 代码复杂但可控性更强 |
2. 时间与空间复杂度对比
- 时间复杂度:两种方法均为O(n),n为节点数,每个节点仅访问一次。
- 空间复杂度:
- 递归:O(h),h为树高,取决于递归栈深度。
- 迭代:O(n),最坏情况下栈需存储所有节点(如链表树)。
3. 大厂面试中的选择策略
- 递归解法:适合快速实现,代码简洁,适合树高较小的场景。
- 迭代解法:适合处理大规模数据,避免栈溢出,体现对数据结构的灵活运用能力。
- 面试建议:当被问及该题时,可先给出递归解法,再主动补充迭代解法,说明两种方法的适用场景。
五、Object栈的潜在风险与优化方案
1. 类型安全风险
- 风险点:若入栈顺序错误(如先压入路径再压入节点),会导致类型转换异常。
- 示例错误:
出栈时会先取节点,再取路径,导致path.push(root.val + ""); // 错误顺序 path.push(root);
(String) path.pop()
抛出ClassCastException
。
2. 优化方案:使用自定义类封装
// 封装节点与路径
class NodePath {TreeNode node;String path;NodePath(TreeNode node, String path) {this.node = node;this.path = path;}
}Stack<NodePath> path = new Stack<>();
path.push(new NodePath(root, root.val + ""));
- 优点:
类型安全,避免强制类型转换,代码可读性更强。 - 缺点:
增加类定义开销,代码量稍增。
3. 性能优化:字符串拼接方式
当前代码使用tempPath + "->" + node.val
,每次拼接都会生成新字符串。可优化为StringBuilder
:
path.push(new NodePath(node, new StringBuilder(tempPath).append("->").append(node.val).toString());
减少字符串对象创建,提升性能。
六、总结:迭代解法的核心思想
1. 栈的双重功能
- 既是节点遍历的容器,也是路径记录的载体,通过Object类型实现多数据类型存储。
2. 出入栈顺序的本质
- 入栈顺序(先右后左)与出栈顺序(先左后右)的配合,本质上是利用栈的LIFO特性模拟深度优先遍历的顺序。
3. 算法设计的核心原则
- 状态绑定:将节点与路径绑定存储,确保遍历过程中路径的实时性。
- 回溯自动化:利用栈的出栈操作自然实现回溯,无需手动管理状态恢复。
这种迭代解法充分展示了数据结构的灵活性——一个看似简单的Object栈,通过精心设计的出入栈顺序,完美模拟了递归的深度优先遍历过程,同时实现了路径的动态构建。理解这种设计思想,对解决类似的路径问题(如二叉树路径和、N叉树路径等)具有重要的借鉴意义。