我们以中序遍历为例,在二叉树:听说递归能做的,栈也能做! (opens new window)中提到说使用栈的话,无法同时解决访问节点(遍历节点)和处理节点(将元素放进结果集)不一致的情况。
那我们就将访问的节点放入栈中,把要处理的节点也放入栈中但是要做标记。
方法一:就是要处理的节点放入栈之后,紧接着放入一个空指针作为标记。 这种方法可以叫做空指针标记法
。
-
前序遍历(根 -> 左 -> 右):
-
压栈顺序:右子节点 -> 左子节点 -> 当前节点 ->
NULL
。
-
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
-> 左子节点。
-
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
-> 右子节点 -> 左子节点。
-
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
:-
表示该节点 已经被安排过,可以直接“收割”(将其值加入结果集)。
-
此时,节点不再需要处理其子节点。
-
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;
}