【码道初阶】【Leetcode94&144&145】二叉树的前中后序遍历(非递归版):显式调用栈的优雅实现

用一个栈搞定二叉树前/中/后序遍历(非递归版):把递归"翻译"成 while 循环

很多人写递归遍历很顺手,但一到非递归就开始迷糊:栈怎么压?什么时候弹?为什么后序还要 prev

其实核心只有一句话:

非递归遍历 = 用显式栈模拟递归调用栈,再用"访问时机"决定前/中/后序。

三种遍历的区别不在"走法",而在"什么时候把节点加入结果"。

  • 前序:第一次见到节点就访问(根最早)
  • 中序:左边走到底后,弹栈时访问(根居中)
  • 后序:左右都处理完,最后才访问(根最晚)(所以需要额外信息判断"右子树是否已经处理过")

下面按你给的三段代码逐段讲。


0. 先统一一个"心智模型":栈里装的是什么?

递归遍历时,每次函数调用会把"当前节点"和"接下来要做的事"压到调用栈里。

非递归就是把"当前节点"手动压到 stack,并用 cur 指针控制下一步去哪。

常见骨架长这样:

java 复制代码
while (cur != null || !stack.isEmpty()) {
    while (cur != null) {
        stack.push(cur);
        cur = cur.left;
    }
    // 到这里说明:左链走到底了
    TreeNode top = stack.pop() or stack.peek();
    // ...访问 or 转向右子树...
}

差别在:

  • 前序:压栈时就访问
  • 中序:弹栈时访问
  • 后序:满足"左右都完成"才弹栈并访问

1) 中序遍历(左-根-右)非递归:最经典模板

中序代码:

java 复制代码
class Solution {
    public List<Integer> inorderTraversal(TreeNode root) {//左根右
        Stack<TreeNode> stack = new Stack<>();
        List<Integer> list = new ArrayList<>();
        TreeNode cur = root;
        while(cur != null || !stack.isEmpty()){
            while(cur != null){
                stack.push(cur);
                cur = cur.left;
            }
            
            TreeNode top = stack.pop();
            list.add(top.val);
            cur = top.right;
        }
        return list;
    }
}

过程讲解(像在走图)

Step A:一路向左"钻到底",沿途压栈

java 复制代码
while(cur != null){
    stack.push(cur);
    cur = cur.left;
}

这一步把从当前节点出发的"左链"全部压栈。

压栈顺序大概就是:根、根的左、左的左......

Step B:左边到头了,开始"回溯"

java 复制代码
TreeNode top = stack.pop();
list.add(top.val);
cur = top.right;
  • pop() 代表:递归里"左子树做完了,回到当前节点"
  • 此时访问 top,就实现了"左-根"
  • 然后 cur = top.right 转向右子树,继续同样流程

为什么是中序?

因为访问发生在 左子树处理完之后、右子树开始之前,刚好是"左根右"的根位置。

✅ 这份代码可以当作中序非递归的"标准答案模板"。


2) 后序遍历(左-右-根)非递归:关键在 prev

后序代码:

java 复制代码
class Solution {
    public List<Integer> postorderTraversal(TreeNode root) {
        List<Integer> list = new ArrayList<>();
        if(root == null) return list;
        Stack<TreeNode> stack = new Stack<>();
        TreeNode cur = root;
        TreeNode prev = null;
        while(cur != null || !stack.isEmpty()){
            while(cur != null){
                stack.push(cur);
                cur = cur.left;
            }
            TreeNode top = stack.peek();
            if(top.right == null || top.right == prev){
                list.add(top.val);
                stack.pop();
                prev = top;
            }else{
                cur = top.right;
            }
        }
        return list;
    }
}

为什么后序比中序难?

后序要"根最后访问"。

但当左链走到底时,栈顶节点的左子树确实做完了------可右子树可能还没做

所以不能像中序那样直接 pop() 访问。

这就是 prev 的意义:

prev 记录"刚刚完成访问(出栈)的节点",用它判断右子树是不是已经处理过。

过程拆解(核心分支只看这一段)

java 复制代码
TreeNode top = stack.peek();
if(top.right == null || top.right == prev){
    // 右子树为空 或 右子树刚处理完
    visit(top); pop(top); prev = top;
}else{
    // 右子树存在且没处理
    cur = top.right;
}

情况 1:top.right == null

右子树为空,那就说明:

  • 左子树已经完成(因为能来到 peek)
  • 右子树不存在
  • 所以当前节点可以访问(根最后)

情况 2:top.right == prev

这句是精髓:

"栈顶节点的右孩子刚刚被访问完",说明右子树已经处理完毕。

于是当前节点也满足"左右都完成",可以访问并出栈。

情况 3:右子树存在且未处理

这时候不能访问根,必须先去右子树:

java 复制代码
cur = top.right;

然后循环会再次走 "一路向左压栈",把右子树的左链压进去。

为什么这一定是后序?

因为每个节点只有在 左子树完成右子树完成 之后才会被访问,根必然最后。

⭐ 易错点重点标识

  • 如果没有 prev,很容易在"从右子树回来"时无法判断是否该访问根,导致反复进入右子树或死循环。
  • peek() 必须配合条件判断;这里不能直接 pop(),否则会提前访问根,顺序变坏。

3) 前序遍历(根-左-右)非递归:访问时机前移

前序代码:

java 复制代码
class Solution {
    public List<Integer> preorderTraversal(TreeNode root) {
        List<Integer> list = new ArrayList<>();
        if(root == null) return list;
        Stack<TreeNode> stack = new Stack<>();
        TreeNode cur = root;
        stack.push(cur);
        while(!stack.isEmpty()){
            while(cur != null){
                stack.push(cur);
                list.add(cur.val);
                cur = cur.left;
            }
            TreeNode top = stack.pop();
            cur = top.right;
        }
        return list;
    }
}

先讲它的核心思想

前序要求"根最先访问"。

所以访问时机要比中序更早:第一次遇到节点就访问

你这份代码确实做到了:在压栈时就 list.add(cur.val)

java 复制代码
while(cur != null){
    stack.push(cur);
    list.add(cur.val);
    cur = cur.left;
}

然后左链走完,弹一个节点出来,转向它的右子树:

java 复制代码
TreeNode top = stack.pop();
cur = top.right;

但这里有个小提醒(实现细节)

这段代码里有一行:

java 复制代码
stack.push(cur); // 进入 while 前又 push 了一次 root

而在循环里又 stack.push(cur),因此根节点会被压栈两次(不过访问只发生一次,所以结果通常不受影响,但栈操作会冗余,逻辑也不够干净)。

更常见、更"干净"的前序非递归写法通常是下面两种之一:

写法 A:一路向左,访问并压栈(不需要额外先 push)

java 复制代码
public List<Integer> preorderTraversal(TreeNode root) {
    List<Integer> res = new ArrayList<>();
    Deque<TreeNode> stack = new ArrayDeque<>();
    TreeNode cur = root;
    while (cur != null || !stack.isEmpty()) {
        while (cur != null) {
            res.add(cur.val);      // 前序:先访问
            stack.push(cur);
            cur = cur.left;
        }
        TreeNode top = stack.pop();
        cur = top.right;
    }
    return res;
}

写法 B:标准"栈模拟"模板(弹出就访问,先压右再压左)

java 复制代码
public List<Integer> preorderTraversal(TreeNode root) {
    List<Integer> res = new ArrayList<>();
    if (root == null) return res;
    Deque<TreeNode> stack = new ArrayDeque<>();
    stack.push(root);
    while (!stack.isEmpty()) {
        TreeNode node = stack.pop();
        res.add(node.val);
        if (node.right != null) stack.push(node.right);
        if (node.left != null) stack.push(node.left);
    }
    return res;
}

两种都对。写法 A 和你中序的骨架更一致,适合"统一模板记忆";写法 B 很直观,适合初学者。


4) 三种遍历的对照总结:差别只在"访问时机"

把三段代码放在一起看,其实只改了一个点:什么时候把 top.val 加进结果。

遍历 顺序 非递归共同骨架 访问时机
前序 根-左-右 左链压栈 + 转右 压栈/第一次遇到节点时访问
中序 左-根-右 左链压栈 + 转右 弹栈时访问(左完成后)
后序 左-右-根 左链压栈 + 条件转右 左右都完成时访问 (靠 prev 判断)

5) 最容易踩的坑(建议贴在笔记顶部)

  1. 后序一定要有"右子树是否处理完"的判断

    没有 prev 或等价机制,极容易死循环或顺序错。

  2. 中序的关键动作是:pop 后访问,再转 right

    少一步都会乱序。

  3. 前序的关键是:访问要发生在走左之前

    add 放到 pop 后,就变成中序/后序味道了。

  4. 尽量用 ArrayDeque 代替 Stack (工程上更推荐)
    Stack 是老类(继承自 Vector),同步开销大;Deque 更现代更快。算法逻辑不变。


结尾:把递归"翻译"成迭代的诀窍

真正的诀窍不是背代码,而是把递归的三件事想明白:

  • 递归"往下走" → 用 curwhile(cur != null) 模拟
  • 递归"回到父节点" → 用 stack 保存路径
  • 前/中/后序的区别 → 访问发生在:下潜前 / 回溯时 / 左右都完成后

理解到这层,换题、换写法都不会慌。

相关推荐
DanyHope2 小时前
《LeetCode 49. 字母异位词分组:哈希表 + 排序 全解析》
算法·leetcode·哈希算法·散列表
iAkuya2 小时前
(leetcode) 力扣100 15轮转数组(环状替代)
数据结构·算法·leetcode
杰克尼2 小时前
蓝桥云课-5. 花灯调整【算法赛】
java·开发语言·算法
.小墨迹2 小时前
C++学习之std::move 的用法与优缺点分析
linux·开发语言·c++·学习·算法·ubuntu
wanghowie2 小时前
01.02 Java基础篇|核心数据结构速查
java·开发语言·数据结构
李斯维2 小时前
安装 WSL 最好的方式
linux·windows
乂爻yiyao2 小时前
java并发演进图
java
java1234_小锋2 小时前
Redis6为什么引入了多线程?
java·redis
努力学算法的蒟蒻2 小时前
day38(12.19)——leetcode面试经典150
算法·leetcode·面试