二叉树的前序遍历(非递归实现)

二叉树的前序遍历顺序是:根节点 → 左子树 → 右子树。递归实现非常简单,但在实际应用中,递归可能导致栈溢出(树深度较大时),因此掌握非递归版本是必要的。这里给出一种利用栈的迭代实现。

算法思路

模拟递归的执行过程。递归函数在调用时会隐式地将当前状态压入系统栈,我们显式地使用一个栈来模拟这个行为。

具体步骤:

  1. 从根节点开始,沿着左子树一直向下走,沿途访问每个节点并将其压入栈中。

  2. 当左子树走到底(当前节点为空)时,从栈中弹出一个节点,表示该节点的左子树已经处理完毕,接下来需要处理它的右子树。

  3. 将当前指针指向弹出节点的右子树,重复步骤1和2,直到栈为空且当前指针也为空。

这个过程中,访问节点的时机是在节点入栈之前(即第一次遇到节点时),这正是前序遍历的定义。

代码实现

cpp

复制代码
class Solution {
public:
    vector<int> preorderTraversal(TreeNode* root) {
        vector<int> result;
        stack<TreeNode*> stk;
        TreeNode* cur = root;

        while (cur != nullptr || !stk.empty()) {
            // 一直向左走,访问并压栈
            while (cur != nullptr) {
                result.push_back(cur->val);   // 访问当前节点(根)
                stk.push(cur);                 // 入栈,后续用来找右子树
                cur = cur->left;                // 继续向左
            }
            // 此时cur为空,说明左子树走完,弹出栈顶节点并转向其右子树
            TreeNode* top = stk.top();
            stk.pop();
            cur = top->right;                   // 处理右子树
        }

        return result;
    }
};

代码解释

  • 外层 while 循环控制整个遍历过程,条件 cur != nullptr || !stk.empty() 保证了当根节点为空或栈中还有待处理的节点时继续执行。

  • 内层 while 循环负责沿着当前节点的左链一直向下,边移动边访问节点,并将节点入栈。这对应了前序遍历先访问根、然后递归左子树的顺序。

  • 当内层循环结束时,cur 变为 nullptr,说明已经到达当前路径的最左端。此时栈顶节点是最后一个被访问的左路节点,它的左子树已经处理完毕,接下来需要处理它的右子树。所以我们弹出该节点,并将 cur 指向它的右孩子,开始下一轮循环。

  • 注意:栈中保存的节点并不是为了再次访问它们(因为已经访问过了),而是为了在左子树完成后能找到它们的右子树入口。

复杂度分析

  • 时间复杂度:O(n),每个节点恰好被访问一次(入栈一次,出栈一次)。

  • 空间复杂度:O(h),h 为树的高度。最坏情况下树退化为链表,空间复杂度 O(n);平均情况下为 O(logn)。

与递归版本的对比

递归版本代码更简洁:

cpp

复制代码
void preorder(TreeNode* root, vector<int>& res) {
    if (!root) return;
    res.push_back(root->val);
    preorder(root->left, res);
    preorder(root->right, res);
}

但递归调用会使用系统栈,深度过大时可能导致栈溢出。非递归版本显式使用栈,可以更好地控制内存,并且在某些语言(如C++)中性能也可能更好。

注意事项

  • 需要处理空树的情况,此时直接返回空数组。

  • 循环条件中的 cur 和栈状态要正确理解:当 cur 为空但栈不为空时,说明还有右子树待处理;当 cur 为空且栈也为空时,遍历结束。

  • 内层循环的 cur = cur->left 可能会导致空指针,但外层循环已经处理了这种情况。

扩展

前序遍历的非递归实现是二叉树迭代遍历的基础,类似的思想可以用于中序遍历(访问时机改为出栈时)和后序遍历(需要更复杂的标记)。理解这个模板有助于解决更多树相关的问题。

相关推荐
郝学胜_神的一滴18 小时前
CMake 30:循环语法全解|foreach_while双循环精讲、迭代技巧与实战避坑指南
c++·cmake
刘马想放假2 天前
Modbus 全栈技术解析:TCP、RTU、ASCII、RTU over TCP
数据结构·网络协议
嘻嘻仙人2 天前
Ubuntu中 git上传自己的项目和二次上传一般流程
git·github
Patrick_Wilson2 天前
Squash Merge 的血缘陷阱:为什么删掉的代码又活了过来
前端·git·程序员
沉浸学习的匿名网友3 天前
什么是 .gitignore?为什么每个 Git 项目几乎都离不开它?
前端·git
北域码匠3 天前
冒泡排序太慢?鸡尾酒排序双向优化,原生 C# 零第三方库完整代码
数据结构·排序算法·泛型·c# 算法·鸡尾酒排序·原生 c# 开发·冒泡排序优化·嵌入式算法
卷无止境3 天前
C++ 的Eigen 库全解析
c++
卷无止境3 天前
现代 C++特性大盘点:一门脱胎换骨的老语言
c++·后端
郝学胜_神的一滴3 天前
CMake 27:缓存变量的特性、语法、类型与实操全解
c++·cmake
深海鱼在掘金4 天前
Git 完全指南 —— 第3章:理解工作区、暂存区、版本库三个核心
git