
个人主页:
<zephyr05>
个人专栏:
座右铭:
等风来不如追风去,追寻的过程就是人生的意义。
目录
[二、前序遍历(根 → 左 → 右)](#二、前序遍历(根 → 左 → 右))
[三、中序遍历(左 → 根 → 右)](#三、中序遍历(左 → 根 → 右))
[四、后序遍历(左 → 右 → 根)](#四、后序遍历(左 → 右 → 根))
一、为什么需要非递归遍历?
递归实现二叉树遍历的代码简洁优雅,三行核心逻辑就能搞定前、中、后序。但递归的本质是依赖系统调用栈,当二叉树退化为链表时(比如深度达到上万层),递归可能会导致栈溢出。此外,在某些对性能敏感的场景中,手动管理栈往往比依赖系统调用栈更高效。
非递归遍历的核心思想很简单:递归能做的,栈也能做。既然递归的底层实现就是栈,那我们完全可以自己用栈来模拟这个过程。
二、前序遍历(根 → 左 → 右)
前序遍历的顺序是:先访问根节点,再访问左子树,最后访问右子树。
算法思路:
-
从根节点开始,一路向左走,沿途访问每个节点并入栈
-
当左孩子为空时,弹出栈顶节点,转向其右子树
-
重复上述过程,直到栈空且当前节点为空
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;
}
三、中序遍历(左 → 根 → 右)
中序遍历的顺序是:先访问左子树,再访问根节点,最后访问右子树。与前序遍历的区别仅在于访问节点的时机不同------前序在入栈时访问,中序在出栈时访问。
算法思路:
-
从根节点开始,一路向左走,沿途节点入栈(不访问)
-
当左孩子为空时,弹出栈顶节点并访问它
-
转向该节点的右子树,重复上述过程
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,转载请注明出处。