序言
递归法求解三种遍历方式是十分简单的,但迭代法却不太简单,本文给出几种方式和模板。
节点类型
struct TreeNode {
int val;
TreeNode* left;
TreeNode* right;
TreeNode(int x) : val(x), left(NULL), right(NULL) {}
};
前序遍历
前序遍历是中左右的循序,根据栈的先入后出的特点,入栈的顺序跟遍历的顺序相反,这样出栈的时候就可以得到想要的顺序。
递归思路:先树根,然后左子树,然后右子树。每棵子树递归。
在迭代算法中,思路演变成,每到一个节点 A,就应该立即访问它。
因为,每棵子树都先访问其根节点。对节点的左右子树来说,也一定是先访问根。在 A 的两棵子树中,遍历完左子树后,再遍历右子树。因此,在访问完根节点后,遍历左子树前,要将右子树压入栈。
class Solution {
public:
vector<int> preorderTraversal(TreeNode* root)
{
stack < TreeNode*> st;
vector< int > result;
st.push(root);
while (!st.empty())
{
TreeNode* node = st.top();
st.pop();
if (node != NULL)
result.push_back(node->val);
else
continue;
st.push(node->right);
st.push(node->left);
}
return result;
}
};
后序遍历
我们可以用与前序遍历相似的方法完成后序遍历。后序遍历与前序遍历相对称。
思路: 每到一个节点 A,就应该立即访问它。 然后将左子树压入栈,再次遍历右子树。
因为前序为中左右,上述完成后为中右左,结果逆序为左右中(后序遍历)
遍历完整棵树后,结果序列逆序即可。
class Solution {
public:
vector<int> postorderTraversal(TreeNode* root)
{
stack < TreeNode*> st;
vector< int > result;
st.push(root);
while (!st.empty())
{
TreeNode* node = st.top();
st.pop();
if (node != NULL)
result.push_back(node->val);
else
continue;
st.push(node->left);
st.push(node->right);
}
reverse(result.begin(), result.end());
return result;
}
};
中序遍历
思路:每到一个节点 A,因为根的访问在中间,将 A 入栈。然后遍历左子树,接着访问 A,最后遍历右子树。
在访问完 A 后,A 就可以出栈了。因为 A 和其左子树都已经访问完成。
class Solution {
public:
vector<int> inorderTraversal(TreeNode* root)
{
stack < TreeNode*> st;
vector< int > result;
TreeNode* node = root;
while (node||st.size())
{
while (node)
{
st.push(node);
node = node->left;
}
if (st.size())
{
node = st.top(), st.pop();
result.push_back(node->val);
node = node->right;
//左子树遍历完成,开始遍历右子树
}
}
return result;
}
};
更巧妙的模板题解
思路:我们需要一个标志区分每个递归调用栈,这里使用nullptr来表示,代表要处理的节点。
刚刚在迭代的过程中,其实我们有两个操作,一个是处理:将元素放进result数组中,一个是访问:遍历节点。
为什么刚刚写的前序遍历的代码,不能和中序遍历通用呢,因为前序遍历的顺序是中左右,要先访问的元素是中间节点,要处理的元素也是中间节点,所以刚刚才能写出相对简洁的代码,因为要访问的元素和要处理的元素顺序是一致的,都是中间节点。
但中序遍历不是,这就造成了处理顺序和访问顺序是不一致的。所以在改进方法里用nullptr标记要处理的结点
前序遍历
class Solution {
public:
vector<int> preorderTraversal(TreeNode* root) {
vector<int> res; //保存结果
stack<TreeNode*> call; //调用栈
if(root!=nullptr) call.push(root); //首先介入root节点
while(!call.empty()){
TreeNode *t = call.top();
call.pop(); //访问过的节点弹出
if(t!=nullptr){
if(t->right) call.push(t->right); //右节点先压栈,最后处理
if(t->left) call.push(t->left);
call.push(t); //当前节点重新压栈(留着以后处理),因为先序遍历所以最后压栈
call.push(nullptr); //在当前节点之前加入一个空节点表示已经访问过了
}else{ //空节点表示之前已经访问过了,现在需要处理除了递归之外的内容
res.push_back(call.top()->val); //call.top()是nullptr之前压栈的一个节点,也就是上面call.push(t)中的那个t
call.pop(); //处理完了,第二次弹出节点(彻底从栈中移除)
}
}
return res;
}
};
来源:力扣(LeetCode)
后序遍历
class Solution {
public:
vector<int> postorderTraversal(TreeNode* root) {
vector<int> res;
stack<TreeNode*> call;
if(root!=nullptr) call.push(root);
while(!call.empty()){
TreeNode *t = call.top();
call.pop();
if(t!=nullptr){
call.push(t); //在右节点之前重新插入该节点,以便在最后处理(访问值)
call.push(nullptr); //nullptr跟随t插入,标识已经访问过,还没有被处理
if(t->right) call.push(t->right);
if(t->left) call.push(t->left);
}else{
res.push_back(call.top()->val);
call.pop();
}
}
return res;
}
};
来源:力扣(LeetCode)
中序遍历
class Solution {
public:
vector<int> inorderTraversal(TreeNode* root) {
vector<int> res;
stack<TreeNode*> call;
if(root!=nullptr) call.push(root);
while(!call.empty()){
TreeNode *t = call.top();
call.pop();
if(t!=nullptr){
if(t->right) call.push(t->right);
call.push(t); //在左节点之前重新插入该节点,以便在左节点之后处理(访问值)
call.push(nullptr); //nullptr跟随t插入,标识已经访问过,还没有被处理
if(t->left) call.push(t->left);
}else{
res.push_back(call.top()->val);
call.pop();
}
}
return res;
}
};
来源:力扣(LeetCode)