我们以中序遍历为例,在二叉树:听说递归能做的,栈也能做! (opens new window)中提到说使用栈的话,无法同时解决访问节点(遍历节点)和处理节点(将元素放进结果集)不一致的情况。
那我们就将访问的节点放入栈中,把要处理的节点也放入栈中但是要做标记。
方法一:就是要处理的节点放入栈之后,紧接着放入一个空指针作为标记。 这种方法可以叫做空指针标记法
。
-
前序遍历(根 -> 左 -> 右):
- 压栈顺序:右子节点 -> 左子节点 -> 当前节点 ->
NULL
。
- 压栈顺序:右子节点 -> 左子节点 -> 当前节点 ->
cpp
class Solution {
public:
vector<int> preorderTraversal(TreeNode* root) {
vector<int> result;
stack<TreeNode*> st;
if(root) st.push(root);
while(!st.empty()){
TreeNode* node = st.top( );
if(node){
st.pop();
if(node->right) st.push(node->right);
if(node->left) st.push(node->left);
st.push(node);
st.push(nullptr);
}else{
st.pop();
node = st.top();
st.pop();
result.push_back(node->val);
}
}
return result;
}
};
-
中序遍历(左 -> 根 -> 右):
- 压栈顺序:右子节点 -> 当前节点 ->
NULL
-> 左子节点。
- 压栈顺序:右子节点 -> 当前节点 ->
cpp
class Solution {
public:
vector<int> inorderTraversal(TreeNode* root) {
vector<int> result;
stack<TreeNode*> st;
if(root) st.push(root);
while(!st.empty()){
TreeNode* node = st.top( );
if(node){
st.pop();
if(node->right) st.push(node->right);
st.push(node);
st.push(nullptr);
if(node->left) st.push(node->left);
}else{
st.pop();
node = st.top();
st.pop();
result.push_back(node->val);
}
}
return result;
}
};
-
后序遍历(左 -> 右 -> 根):
- 压栈顺序:当前节点 ->
NULL
-> 右子节点 -> 左子节点。
- 压栈顺序:当前节点 ->
cpp
class Solution {
public:
vector<int> postorderTraversal(TreeNode* root) {
vector<int> result;
stack<TreeNode*> st;
if(root) st.push(root);
while(!st.empty()){
TreeNode* node = st.top( );
if(node){
st.pop();
st.push(node);
st.push(nullptr);
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;
}
};
方法二:加一个 boolean
值跟随每个节点,false
(默认值) 表示需要为该节点和它的左右儿子安排在栈中的位次,true
表示该节点的位次之前已经安排过了,可以收割节点了。 这种方法可以叫做boolean 标记法
,样例代码见下文C++ 和 Python 的 boolean 标记法
。 这种方法更容易理解,在面试中更容易写出来。
Boolean 标记法的核心思想
-
标记的作用:
-
false
:表示该节点需要被"安排"(即需要处理其子节点)。 -
true
:表示该节点已经被"安排"过,可以直接收割(加入结果集)。
-
-
栈的使用:
- 栈中存储的是
pair<TreeNode*, bool>
,其中bool
表示节点的状态。
- 栈中存储的是
-
遍历顺序:
- 根据前序、中序、后序遍历的要求,调整节点的压栈顺序。
visited
是一个布尔值(bool
),用于标记当前节点是否已经被"处理"过。它的作用是区分节点的两种状态:
-
visited = false
:-
表示该节点 需要被安排,即需要处理其子节点。
-
此时,节点会被重新压入栈中,并按照遍历顺序(前序、中序、后序)调整其子节点的压栈顺序。
-
-
visited = true
:-
表示该节点 已经被安排过,可以直接"收割"(将其值加入结果集)。
-
此时,节点不再需要处理其子节点。
-
cpp
vector<int> traversal(TreeNode* root) {
vector<int> result;
if (!root) return result;
stack<pair<TreeNode*, bool>> st;
st.push({root, false}); // 初始状态为 false,表示需要安排子节点
while (!st.empty()) {
auto [node, visited] = st.top();
st.pop();
if (visited) {
// 如果节点已经被访问过,直接收割
result.push_back(node->val);
} else {
// 根据遍历顺序调整压栈顺序
// 前序:根 -> 左 -> 右
// 中序:左 -> 根 -> 右
// 后序:左 -> 右 -> 根
if (node->right) st.push({node->right, false}); // 右子节点
st.push({node, true}); // 当前节点标记为已访问
if (node->left) st.push({node->left, false}); // 左子节点
}
}
return result;
}