算法随想Day12【二叉树】| LC144、LC145、LC94-二叉树的前中后序遍历

LC144、LC145、LC94-二叉树的前中后遍历

二叉树递归遍历

比较容易实现

struct TreeNode
{
    int val;
    TreeNode* left;
    TreeNode* right;
    TreeNode(int x = 0) : val(x), left(nullptr), right(nullptr) {  }
};

class Tree
{
public:
    vector<int> preorderTraversal(TreeNode* root)
    {
        vector<int> vct;
        pre_traversal(root, vct);
        return vct;
    }
    void pre_traversal(TreeNode* curr, vector<int>& vct)
    {
        if(curr == nullptr)
        {
            return;
        }
        // 前序遍历--中左右
        vct.emplace_back(curr->val);
        pre_traversal(curr->left, vct);
        pre_traversal(curr->right, vct);   
    }

    vector<int> inorderTraversal(TreeNode* root)
    {
        vector<int> vct;
        in_traversal(root, vct);
        return vct;
    }
    void in_traversal(TreeNode* curr, vector<int>& vct)
    {
        if(curr == nullptr)
        {
            return;
        }
        // 中序遍历--左中右
        in_traversal(curr->left, vct);
        vct.emplace_back(curr->val);
        in_traversal(curr->right, vct);   
    }

    vector<int> postorderTraversal(TreeNode* root)
    {
        vector<int> vct;
        post_traversal(root, vct);
        return vct;
    }
    void post_traversal(TreeNode* curr, vector<int>& vct)
    {
        if(curr == nullptr)
        {
            return;
        }
        // 后序遍历--左右中
        post_traversal(curr->left, vct);
        vct.emplace_back(curr->val);
        post_traversal(curr->right, vct);   
    }      
};

二叉树非递归遍历

迭代法需要通过使用“栈”来模拟递归的过程

前序遍历:

放入root节点,弹出每个节点时,依次push入该节点的右孩子和左孩子

后序遍历:

由中左右->中右左,翻转结果中右左->左右中,即为后续遍历

中序遍历:

无法像上述后序由前序调换“左右”位置即得到结果一样,因为对"中"节点的访问顺序和处理顺序不一致。

class Tree
{
public:
    // 前序遍历--中左右
    vector<int> preorderTraversal_iteration(TreeNode* root)
    {
        vector<int> vct;
        stack<TreeNode*> stk;

        if(root == nullptr) 
            return vct;

        stk.push(root);

        while(!stk.empty())
        {   
            TreeNode* curr = stk.top();
            vct.emplace_back(curr->val);
            stk.pop();

            if(curr->right) stk.push(curr->right);
            if(curr->left) stk.push(curr->left);
        }

        return vct;
    }

    // 后序遍历--左右中,即前序中的中左右,现改为中右左,在将vct整体镜像翻转所得
    vector<int> postorderTraversal_iteration(TreeNode* root)
    {
        vector<int> vct;
        stack<TreeNode*> stk;

        if(root == nullptr)
        {
            return vct;
        }

        stk.push(root);

        while(!stk.empty())
        {   
            TreeNode* curr = stk.top();
            vct.emplace_back(curr->val);
            stk.pop();

            if(curr->left) stk.push(curr->left);
            if(curr->right) stk.push(curr->right);
        }

        reverse(vct.begin(), vct.end());
        return vct;
    }
};

二叉树非递归遍历(统一写法)

每个节点都会经历三个阶段:

  • 被访问,push入栈
  • 被处理,节点value放入vector
  • pop出栈

前序遍历(中左右 -- 访问和处理同步):

  • 处理的时机:访问的同时,即可处理。如最开始访问root时,就可以sta.push()
  • pop的时机:对某个节点,处理完左子树,并回溯时,用来找到右子树后,即可pop

中序遍历(左中右 -- 访问和处理不同步):

  • 处理的时机:如root被访问并push入栈后,没有立即处理,而是一路向左直到访问其left孩子不存在时,才会回溯处理
  • pop的时机:回溯处理后,下一步是访问其右子树,用来找到right孩子后,即可pop

后序遍历(左右中 -- 访问和处理不同步):

  • 处理的时机:对某个节点,被访问后push入栈。没有立即处理,一路向左直到访问其left孩子不存在时,撤回访问其right孩子,这两步都结束了才再次撤回处理该节点。

  • pop的时机:对某个节点,第二次回溯时,处理并pop

  • 后序遍历在实现上有更多的细节需要注意:因为后序遍历中,对每个节点都需要两次回溯,第一次是判断有无right孩子,第二次才是处理该节点。所以需要引入了一个prev指针来记录节点的孩子的处理历史,对访问到的某个节点来说,当访问完它的一棵子树时,用prev指向该子树的父节点,这样当回溯到这个节点的时候,就可以依据prev是指向左子节点,还是右子节点,来判断父节点的访问情况。

分析前中后序迭代法的Uniform写法的异同

(因为树的遍历规定,访问左子树的顺序优于右子树,所以规定让代码大部分思路是统一的):

  • 在循环前的定义中,栈都保留为空,curr为当前访问的节点,指向root
  • 循环条件都是判断curr不为空,或栈不为空
  • 循环内当left不为空时,都会一直向左访问下去
  • 基于上个条件,当curr为空时,会进入循环中的else分支,分支内都会取当前栈顶的元素作为curr

​ 参考上面描述的处理节点值和pop出节点的时机

  • 前序和中序,除了对节点的处理时机不同,其他代码都一致
  • 后序相对前序和中序,在回溯时多了一步,即判断当前节点的右子树是否已经访问完
////前序遍历--中左右
vector<int> preorderTraversal_uniform(TreeNode *root)
{
    vector<int> result;
    stack<TreeNode*> sta;
    TreeNode* curr = root;
    while(curr != nullptr || sta.empty() != true)
    {
        if (curr != nullptr)
        {
            result.push_back(curr->val);
            sta.push(curr);
            curr = curr->left;
        }
        else
        {
            curr = sta.top();
            sta.pop();
            curr = curr->right;
        }
    }

    return result;
}

////中序遍历--左中右
vector<int> inorderTraversal_iteration(TreeNode* root)
{
    vector<int> result;
    stack<TreeNode*> sta;  // 栈中元素代表遍历过的,栈顶表示当前访问的节点
    TreeNode* curr = root;
    while(curr != nullptr || sta.empty() != true)
    {   
        if(curr != nullptr)  // 有左子节点,压入,并将左子节点当作新的当前根节点
        {
            sta.push(curr);
            curr = curr->left;
        }
        else  // 发现没左子节点了,退一步改将当前根节点改回上一步,并pop出,并将其右子节点当作新的当前根节点
        {
            curr = sta.top();
            sta.pop();
            result.push_back(curr->val);
            curr = curr->right;
        }
    }

    return result;
}

////后序遍历--左右中
vector<int> postorderTraversal_uniform(TreeNode *root)
{
    vector<int> result;
    stack<TreeNode*> sta;
    TreeNode* curr = root;
    TreeNode* prev = nullptr;
    while (curr != nullptr || sta.empty() != true)
    {
        if (curr != nullptr)
        {
            sta.push(curr);
            curr = curr->left;
        }
        else
        {
            curr = sta.top();
            ////如果没有右子树,或者右子树访问完了--即上一个访问的节点是右子节点时
            ////说明可以处理当前节点,处理完后,设置该节点为prev,好让回溯该节点的
            ////父节点时,知道上步处理了父节点的left还是right孩子。同时要pop()出
            ////当前节点,在下次重新top()获得的即为回溯后的父节点,更重要的细节是
            ////不管当前节点对父节点来说是left或right,都需要curr=nullptr,因为
            ////到了这步,当前节点必定已经处理完了左子树,所以防止在下次循环中再次
            ////访问左节点,造成死循环
            if (curr->right == nullptr || curr->right == prev)
            {
                result.push_back(curr->val);
                prev = curr;
                sta.pop();
                curr = nullptr;
            }
            else
            {
                curr = curr->right;
            }
        }

    }
    return result;
}
posted @ 2023-02-14 23:09  冥紫将  阅读(28)  评论(0)    收藏  举报