LEETCODE 094Easy
二叉树的中序遍历
递归天然记住了“回来以后接着往下走”,显式栈就得自己把这件事补上。
问题拆解
中序遍历的规则是:先访问左子树,再访问根,最后访问右子树。递归写法几乎不用动脑,但题目进阶要求用迭代完成,这才是值得琢磨的地方。
难点在于:递归之所以简单,是因为函数调用栈帮我们记住了“处理完左子树后,还得回到当前节点、再去处理右子树”。改成迭代,就得自己用一个栈把这条“回头路”显式管理起来。
迭代版遍历的本质,是手动复刻递归调用栈干的活。
递归写法
public List<Integer> inorderTraversal(TreeNode root) {
List<Integer> result = new ArrayList<>();
visit(root, result);
return result;
}
private void visit(TreeNode node, List<Integer> result) {
if (node == null) return;
visit(node.left, result);
result.add(node.val);
visit(node.right, result);
}
def inorderTraversal(root):
result = []
def visit(node):
if not node:
return
visit(node.left)
result.append(node.val)
visit(node.right)
visit(root)
return result
func inorderTraversal(root *TreeNode) []int {
result := []int{}
var visit func(node *TreeNode)
visit = func(node *TreeNode) {
if node == nil {
return
}
visit(node.Left)
result = append(result, node.Val)
visit(node.Right)
}
visit(root)
return result
}
pub fn inorder_traversal(root: Option<Rc<RefCell<TreeNode>>>) -> Vec<i32> {
let mut result = Vec::new();
fn visit(node: &Option<Rc<RefCell<TreeNode>>>, result: &mut Vec<i32>) {
if let Some(n) = node {
let n = n.borrow();
visit(&n.left, result);
result.push(n.val);
visit(&n.right, result);
}
}
visit(&root, &mut result);
result
}
三行顺序就是中序定义本身,改一改顺序就能得到前序或后序。
显式栈迭代
用一个指针 cur 一路向左把节点压栈,走到底后弹出访问,再转向它的右子树:
public List<Integer> inorderTraversal(TreeNode root) {
List<Integer> result = new ArrayList<>();
Deque<TreeNode> stack = new ArrayDeque<>();
TreeNode cur = root;
while (cur != null || !stack.isEmpty()) {
while (cur != null) {
stack.push(cur);
cur = cur.left;
}
cur = stack.pop();
result.add(cur.val);
cur = cur.right;
}
return result;
}
def inorderTraversal(root):
result, stack = [], []
cur = root
while cur or stack:
while cur:
stack.append(cur)
cur = cur.left
cur = stack.pop()
result.append(cur.val)
cur = cur.right
return result
func inorderTraversal(root *TreeNode) []int {
result := []int{}
stack := []*TreeNode{}
cur := root
for cur != nil || len(stack) > 0 {
for cur != nil {
stack = append(stack, cur)
cur = cur.Left
}
cur = stack[len(stack)-1]
stack = stack[:len(stack)-1]
result = append(result, cur.Val)
cur = cur.Right
}
return result
}
pub fn inorder_traversal(root: Option<Rc<RefCell<TreeNode>>>) -> Vec<i32> {
let mut result = Vec::new();
let mut stack = Vec::new();
let mut cur = root;
while cur.is_some() || !stack.is_empty() {
while let Some(node) = cur {
cur = node.borrow().left.clone();
stack.push(node);
}
let node = stack.pop().unwrap();
result.push(node.borrow().val);
cur = node.borrow().right.clone();
}
result
}
内层循环负责“尽量往左”,弹栈时机就对应递归里“左子树处理完了”的那一刻,访问完当前值后把 cur 指向右孩子,循环会自动对右子树重复同样的过程。
复杂度
| 指标 | 复杂度 | 原因 |
|---|---|---|
| 时间 | O(n) |
每个节点入栈、出栈各一次 |
| 空间 | O(h) |
栈里最多存一条从根到当前节点的路径 |
可以迁移的模式
- 任何递归都能用显式栈改写成迭代,关键是想清楚“栈里存的是待返回的现场”;
- “一路向某个方向走到底,再回退”的结构,天然对应压栈—弹栈;
- 遍历顺序的差异只在访问根节点的时机,框架可以复用。
当递归深度可能失控、或面试要求手写栈时,这套“模拟调用栈”的思路就派上用场了。