二叉树遍历之统一迭代法:轻松实现前中后序遍历
在二叉树的遍历中,我们常常会遇到前序遍历、中序遍历和后序遍历这三种方式。递归是一种很直观的实现方法,但迭代法也有其独特的优势。今天我们要介绍一种二叉树遍历的统一迭代法,它可以像递归法通过调整顺序那样,轻松实现前、中、后三种遍历方式,是不是很神奇呢?下面就来详细看看吧。
二叉树遍历基础
(一)前序遍历(Pre - order Traversal)
顺序是根节点、左子树、右子树。对于给定的二叉树,先访问根节点,然后递归地遍历左子树,最后递归地遍历右子树。例如,对于二叉树:
1
/ \
2 3
/ \ / \
4 5 6 7
前序遍历的结果是 1 2 4 5 3 6 7
。
(二)中序遍历(In - order Traversal)
顺序是左子树、根节点、右子树。先递归遍历左子树,然后访问根节点,最后递归遍历右子树。上述二叉树的中序遍历结果是 4 2 5 1 6 3 7
。
(三)后序遍历(Post - order Traversal)
顺序是左子树、右子树、根节点。先递归遍历左子树,然后递归遍历右子树,最后访问根节点。此二叉树的后序遍历结果是 4 5 2 6 7 3 1
。
统一迭代法原理
核心思想
统一迭代法的核心是利用栈来模拟递归的过程。我们知道,递归是通过函数调用栈来实现的,而这里我们手动用栈来控制节点的访问顺序。关键在于,对于不同的遍历顺序(前、中、后序),我们按照与遍历方向相反的顺序将节点压入栈中。
解题关键(中序为例)
1. 中序遍历的顺序特点
中序遍历二叉树的顺序是先访问左子树,然后访问根节点,最后访问右子树。这是整个解题思路围绕的核心规则,我们要通过迭代的方式模拟出这个顺序的访问过程。
2. 利用栈来辅助实现
栈是实现中序遍历统一迭代法的关键数据结构。它具有后进先出(LIFO)的特性,能够帮助我们按照特定的顺序处理二叉树的节点。
3. 入栈顺序与标记处理
- 入栈方向与遍历方向相反:
- 在中序遍历中,实际访问顺序是左子树、根节点、右子树,但我们入栈时要按照右子树、根节点、左子树的顺序进行。这是因为栈的后进先出特性,我们先把右子树压入栈,后续出栈时它就会在后面才被处理,从而能先处理左子树,符合中序遍历先左后右再根的顺序。
- 例如,对于一个二叉树节点
node
,当它不为空时,我们先将其右子节点(如果存在)通过if (node->right) st.push(node->right);
压入栈。
- 标记已处理节点:
- 在把根节点压入栈后,我们紧接着压入一个特殊的标记
NULL
,即st.push(NULL);
。这个标记的作用是用来标识当前根节点虽然已经被压入栈,但还没有真正完成中序遍历意义上的处理(也就是还没有将其值添加到结果集中)。 - 然后再将左子节点(如果存在)通过
if (node->left) st.push(node->left);
压入栈。
- 在把根节点压入栈后,我们紧接着压入一个特殊的标记
4. 出栈与结果收集
- 处理标记节点:
- 在循环过程中,当栈顶元素为
NULL
时,这就表示遇到了之前压入的标记节点,意味着前面压入的那个根节点现在可以进行真正的中序遍历处理了。 - 此时,我们先将这个
NULL
标记弹出栈,即st.pop();
。
- 在循环过程中,当栈顶元素为
- 获取并处理根节点:
- 接着,再取出栈顶的节点(此时栈顶就是之前压入的那个根节点了),存到一个临时变量
node
中,然后再把这个节点从栈顶弹出,即通过node = st.top(); st.pop();
操作。 - 最后,将这个根节点的
val
值通过result.push_back(node->val);
添加到结果集result
中,这样就完成了一个节点的中序遍历处理,将其值按照中序遍历的顺序放入了结果集中。
- 接着,再取出栈顶的节点(此时栈顶就是之前压入的那个根节点了),存到一个临时变量
5. 整体循环控制
通过一个 while
循环来不断地处理栈中的节点,只要栈 st
不为空,循环就会持续进行,即 while (!st.empty())
。在每次循环中,根据栈顶节点的情况(是普通节点还是标记节点)进行上述相应的操作,从而逐步完成整个二叉树的中序遍历,最终返回存储着中序遍历结果的 result
向量。
总的来说,中序遍历统一迭代法的关键思路就是利用栈的特性,通过巧妙地设置入栈顺序(与遍历顺序相反)并借助特殊标记来处理节点,按照中序遍历的规则准确地将节点值收集到结果集中,实现二叉树的中序遍历过程。
94. 二叉树的中序遍历
当节点不为空时,我们先弹出当前节点,然后将右子节点(如果存在)、当前节点、一个空标记(用于标记节点已经被处理过一次,但还没输出)、左子节点(如果存在)依次压入栈。当遇到空标记时,说明当前节点的左子树已经处理完,此时弹出栈顶节点(即当前节点)并将其值加入结果集。这样就实现了中序遍历,按照左 - 中 - 右的顺序处理节点。
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
public:
vector<int> inorderTraversal(TreeNode* root) {
vector<int> result;//存放最终返回的遍历结果
stack<TreeNode*> st;//将待处理节点放入栈中存储
if(root!=NULL) st.push(root);
while(!st.empty()){
TreeNode* node=st.top();
if(node!=NULL){
st.pop();
//入栈方向和遍历方向相反,中序:左中右;入栈:右中左
if(node->right) st.push(node->right);
st.push(node);
st.push(NULL);
if(node->left) st.push(node->left);
}else{
st.pop();
node=st.top();
st.pop();
result.push_back(node->val);
}
}
return result;
}
};
144. 二叉树的前序遍历
对于前序遍历,当节点不为空时,先弹出当前节点,然后将右子节点、左子节点、当前节点、空标记依次压入栈。当遇到空标记时,弹出当前节点并加入结果集,这样就实现了先访问根节点,再访问左子树和右子树的前序遍历顺序(中 - 左 - 右)。
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
public:
vector<int> preorderTraversal(TreeNode* root) {
vector<int> result;//存放最终返回的遍历结果
stack<TreeNode*> st;//将待处理节点放入栈中存储
if(root!=NULL) st.push(root);
while(!st.empty()){
TreeNode* node=st.top();
if(node!=NULL){
st.pop();
//入栈方向和遍历方向相反,前序:中左右;入栈:右左中
if(node->right) st.push(node->right);
if(node->left) st.push(node->left);
st.push(node);
st.push(NULL);
}else{
st.pop();
node=st.top();
st.pop();
result.push_back(node->val);
}
}
return result;
}
};
145. 二叉树的后序遍历
在后序遍历中,当节点不为空时,先弹出当前节点,然后将当前节点、空标记、右子节点、左子节点依次压入栈。当遇到空标记时,弹出当前节点并加入结果集,从而实现了左 - 右 - 中的后序遍历顺序。
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
public:
vector<int> postorderTraversal(TreeNode* root) {
vector<int> result;//存放最终返回的遍历结果
stack<TreeNode*> st;//将待处理节点放入栈中存储
if(root!=NULL) st.push(root);
while(!st.empty()){
TreeNode* node=st.top();
if(node!=NULL){
st.pop();
//入栈方向和遍历方向相反,后序:左右中;入栈:中右左
st.push(node);
st.push(NULL);
if(node->right) st.push(node->right);
if(node->left) st.push(node->left);
}else{
st.pop();
node=st.top();
st.pop();
result.push_back(node->val);
}
}
return result;
}
};
总结
通过这种统一迭代法,我们巧妙地利用栈实现了二叉树的前、中、后序遍历。它的优点在于,我们不需要像递归那样依赖系统的函数调用栈,对于一些对栈空间有要求的场景可能更适用。而且这种统一的方法,让我们只需要记住一个基本的框架,通过调整入栈的顺序,就可以轻松实现不同的遍历方式,大大提高了代码的复用性和可维护性。希望大家通过本文对二叉树的遍历有更深入的理解和掌握。
参考资料
代码随想录