从递归到迭代:二叉树非递归前中后序遍历详解

个人主页:

<zephyr05>

个人专栏:

<Linux> <C/C++> <竞赛专栏> <数据结构>

座右铭:

等风来不如追风去,追寻的过程就是人生的意义。


目录

一、为什么需要非递归遍历?

[二、前序遍历(根 → 左 → 右)](#二、前序遍历(根 → 左 → 右))

[三、中序遍历(左 → 根 → 右)](#三、中序遍历(左 → 根 → 右))

[四、后序遍历(左 → 右 → 根)](#四、后序遍历(左 → 右 → 根))

五、复杂度分析

六、总结

七、完整代码以及测试示例


一、为什么需要非递归遍历?

递归实现二叉树遍历的代码简洁优雅,三行核心逻辑就能搞定前、中、后序。但递归的本质是依赖系统调用栈,当二叉树退化为链表时(比如深度达到上万层),递归可能会导致栈溢出。此外,在某些对性能敏感的场景中,手动管理栈往往比依赖系统调用栈更高效。

非递归遍历的核心思想很简单:递归能做的,栈也能做。既然递归的底层实现就是栈,那我们完全可以自己用栈来模拟这个过程。

二、前序遍历(根 → 左 → 右)

前序遍历的顺序是:先访问根节点,再访问左子树,最后访问右子树。

算法思路

  1. 从根节点开始,一路向左走,沿途访问每个节点并入栈

  2. 当左孩子为空时,弹出栈顶节点,转向其右子树

  3. 重复上述过程,直到栈空且当前节点为空

cpp 复制代码
vector<int> preorderTraversal(TreeNode* root) {
    vector<int> result;
    stack<TreeNode*> st;
    TreeNode* cur = root;
    
    while (cur || !st.empty()) {
        // 一路向左,访问并入栈
        while (cur) {
            result.push_back(cur->val);  // 先访问根
            st.push(cur);
            cur = cur->left;
        }
        // 左子树为空,弹出栈顶,转向右子树
        cur = st.top();
        st.pop();
        cur = cur->right;
    }
    return result;
}

另一种更简洁的写法:

cpp 复制代码
vector<int> preorderTraversal(TreeNode* root) {
    vector<int> result;
    if (!root) return result;
    
    stack<TreeNode*> st;
    st.push(root);
    
    while (!st.empty()) {
        TreeNode* node = st.top();
        st.pop();
        result.push_back(node->val);
        
        // 先压右孩子,再压左孩子(左孩子先出栈)
        if (node->right) st.push(node->right);
        if (node->left) st.push(node->left);
    }
    return result;
}

三、中序遍历(左 → 根 → 右)

中序遍历的顺序是:先访问左子树,再访问根节点,最后访问右子树。与前序遍历的区别仅在于访问节点的时机不同------前序在入栈时访问,中序在出栈时访问。

算法思路

  1. 从根节点开始,一路向左走,沿途节点入栈(不访问

  2. 当左孩子为空时,弹出栈顶节点并访问它

  3. 转向该节点的右子树,重复上述过程

cpp 复制代码
vector<int> inorderTraversal(TreeNode* root) {
    vector<int> result;
    stack<TreeNode*> st;
    TreeNode* cur = root;
    
    while (cur || !st.empty()) {
        // 一路向左,只入栈不访问
        while (cur) {
            st.push(cur);
            cur = cur->left;
        }
        // 左子树为空,弹出并访问
        cur = st.top();
        st.pop();
        result.push_back(cur->val);  // 出栈时访问
        cur = cur->right;
    }
    return result;
}

对比前序 :前序和中序的代码结构几乎完全一样,唯一的区别是 result.push_back() 的位置------前序在入栈时,中序在出栈时。

四、后序遍历(左 → 右 → 根)

后序遍历的顺序是:先访问左子树,再访问右子树,最后访问根节点。这是三种遍历中实现最复杂的一种,因为根节点需要在左右子树都访问完毕之后才能输出。

双栈法(推荐,最容易理解)

双栈法利用两次反转来得到后序结果:

  • 第一个栈按 "根 → 右 → 左" 的顺序遍历(这是前序遍历的变体)

  • 将遍历结果压入第二个栈

  • 从第二个栈弹出的顺序正好是 "左 → 右 → 根"

cpp 复制代码
vector<int> postorderTraversal(TreeNode* root) {
    vector<int> result;
    if (!root) return result;
    
    stack<TreeNode*> s1, s2;
    s1.push(root);
    
    // s1 按 "根 -> 右 -> 左" 的顺序遍历
    while (!s1.empty()) {
        TreeNode* node = s1.top();
        s1.pop();
        s2.push(node);  // 压入s2
        
        // 注意:先压左再压右,这样s1弹出时先右后左
        if (node->left) s1.push(node->left);
        if (node->right) s1.push(node->right);
    }
    
    // s2 弹出顺序即为 "左 -> 右 -> 根"
    while (!s2.empty()) {
        result.push_back(s2.top()->val);
        s2.pop();
    }
    return result;
}

后序遍历要求"左→右→根",而前序变体"根→右→左"正好是它的逆序。利用栈的先进后出特性,把"根→右→左"压入第二个栈,弹出来就变成了"左→右→根"。双栈法虽然多用一个栈,但逻辑清晰,不容易出错。

五、复杂度分析

遍历方式 时间复杂度 空间复杂度
前序 O(n) O(h)
中序 O(n) O(h)
后序(双栈) O(n) O(n)

其中 n 为节点数,h 为树的高度。最坏情况下(退化为链表),h = n,空间复杂度为 O(n)。

六、总结

遍历 访问时机 核心技巧
前序 入栈时访问 一路向左,边入栈边访问
中序 出栈时访问 一路向左入栈,出栈时访问
后序 左右子树都访问后 双栈法

三种非递归遍历本质上都是在模拟递归调用栈的过程。理解了这一点,你会发现它们不过是访问时机的差异而已。建议在学习时画一棵二叉树,手动模拟栈的变化过程,理解会更深刻。

七、完整代码以及测试示例

cpp 复制代码
#include <iostream>
#include <stack>
#include <vector>
using namespace std;

// 二叉树节点定义
struct TreeNode {
    int val;
    TreeNode *left;
    TreeNode *right;
    TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
};

// ---------- 非递归前序遍历 ----------
vector<int> preorderTraversal(TreeNode* root) {
    vector<int> result;
    if (!root) return result;

    stack<TreeNode*> st;
    st.push(root);

    while (!st.empty()) {
        TreeNode* node = st.top();
        st.pop();
        result.push_back(node->val);

        // 先压右孩子,再压左孩子(确保左孩子先出栈)
        if (node->right) st.push(node->right);
        if (node->left) st.push(node->left);
    }
    return result;
}

// ---------- 非递归中序遍历 ----------
vector<int> inorderTraversal(TreeNode* root) {
    vector<int> result;
    stack<TreeNode*> st;
    TreeNode* cur = root;

    while (cur || !st.empty()) {
        // 一直向左,沿途节点入栈
        while (cur) {
            st.push(cur);
            cur = cur->left;
        }
        // 当前节点为空,弹出栈顶并访问
        cur = st.top();
        st.pop();
        result.push_back(cur->val);
        // 转向右子树
        cur = cur->right;
    }
    return result;
}

// ---------- 非递归后序遍历(双栈法) ----------
vector<int> postorderTraversal(TreeNode* root) {
    vector<int> result;
    if (!root) return result;

    stack<TreeNode*> st1, st2;
    st1.push(root);

    // 先按"根-右-左"的顺序压入st2,最后st2出栈即为"左-右-根"
    while (!st1.empty()) {
        TreeNode* node = st1.top();
        st1.pop();
        st2.push(node);

        if (node->left) st1.push(node->left);
        if (node->right) st1.push(node->right);
    }

    while (!st2.empty()) {
        result.push_back(st2.top()->val);
        st2.pop();
    }
    return result;
}

// ---------- 辅助函数:打印遍历结果 ----------
void printTraversal(const vector<int>& vec, const string& name) {
    cout << name << ": ";
    for (int val : vec) {
        cout << val << " ";
    }
    cout << endl;
}

// ---------- 测试 ----------
int main() {
    // 构造一棵二叉树:
    //        1
    //       / \
    //      2   3
    //     / \
    //    4   5
    TreeNode* root = new TreeNode(1);
    root->left = new TreeNode(2);
    root->right = new TreeNode(3);
    root->left->left = new TreeNode(4);
    root->left->right = new TreeNode(5);

    // 前序:1 2 4 5 3
    vector<int> pre = preorderTraversal(root);
    printTraversal(pre, "前序");

    // 中序:4 2 5 1 3
    vector<int> in = inorderTraversal(root);
    printTraversal(in, "中序");

    // 后序:4 5 2 3 1
    vector<int> post = postorderTraversal(root);
    printTraversal(post, "后序");

    // 释放内存(略)
    return 0;
}

如果本文对你有帮助,欢迎点赞、收藏、分享。如有疑问,请在评论区留言,我会尽力解答。

博客首发于 CSDN,转载请注明出处。

相关推荐
Dr.kangder1 小时前
嵌入式总线设备解析——TTE总线应用与实践
开发语言·网络·算法·嵌入式·多任务·同步机制
Hi李耶1 小时前
【LeetCode】557.反转字符串中的单词 III
算法·leetcode·职场和发展
深蓝学院1 小时前
机器人学习算法五大体系详解:模仿、强化、多模态、持续学习……
算法·机器人
2401_858286112 小时前
OS82.【Linux】设计线程池
java·linux·运维·服务器·开发语言·算法·线程池
Epiphany.5562 小时前
找到所有好字符串(记忆化搜索+KMP)(里面含KMP模板,用来入门)
算法
坚持编程的菜鸟2 小时前
模拟实现strncpy
c语言·算法·模拟实现strncpy
深圳市快瞳科技有限公司4 小时前
个体识别、行为解读、健康管理:多模态宠物AI大模型的场景化落地
人工智能·算法·计算机视觉·大模型·多模态·宠物·宠物ai识别
liulilittle4 小时前
归一化:激活函数
人工智能·算法·机器学习·llm
Mem0rin6 小时前
前缀和:一维与二维
数据结构·算法