• 【算法练习Day12】树的递归遍历&&非递归遍历


    在这里插入图片描述

    ​📝个人主页@Sherry的成长之路
    🏠学习社区:Sherry的成长之路(个人社区)
    📖专栏链接:练题
    🎯长路漫漫浩浩,万事皆有期待

    这一期讲树这个数据结构的相关知识

    首先我们要明白树的两种通用遍历分别是深度优先搜索,和广度优先搜索。这里我们介绍深度优先搜索的三种表现形式:前序遍历,中序遍历和后序遍历。这三种搜索方式可以用递归法或者迭代法表示出来。事实上,很多递归能写出来的代码,大都可以使用迭代法表示出来。

    递归遍历

    前序遍历

    class Solution 
    {
    public:
    vector<int>result;
    void dfs(TreeNode* root)
    {
        if(root)
        result.push_back(root->val);
        if(root)
        dfs(root->left);
        if(root)
        dfs(root->right);
    }
        vector<int> preorderTraversal(TreeNode* root) 
        {
            if(root==nullptr)
            return result;
            dfs(root);
            return result;
        }
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    前序遍历的规则是“中左右“,即先遍历树的中间节点,再分别遍历左右两子树,并在其遍历左右两子树时仍然遵循此规则。所以我们可以容易的理解dfs代码部分先将中间节点保存后,分别进行左子树和右子树的递归。

    中序遍历和后序遍历的递归代码,都和前序遍历差不多,只是略微调整一下进入子树的时机而已,下面直接给出代码。

    中序遍历

    class Solution 
    {
    public:
    	void dfs(TreeNode* root,vector<int>& result)
    	{
    	    if(root==nullptr)return ;
    	    dfs(root->left,result);
    	    result.push_back(root->val);
    	    dfs(root->right,result);
    	}
        vector<int> inorderTraversal(TreeNode* root) 
        {
            vector<int>result;
            dfs(root,result);
            return result;
        }
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    后序遍历

    class Solution 
    {
    public:
    	void dfs(TreeNode* root,vector<int>& result)
    	{
    	    if(root==nullptr)return ;
    	    dfs(root->left,result);
    	    dfs(root->right,result);
    	    result.push_back(root->val);
    	}
        vector<int> inorderTraversal(TreeNode* root) 
        {
            vector<int>result;
            dfs(root,result);
            return result;
        }
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    说完了递归遍历,我们再来看看非递归遍历

    非递归遍历

    非递归遍历中的迭代遍历,前后序的代码是差不多的,但是中序遍历有很大差别

    前序遍历

    先说前序遍历的迭代,思路是用一个栈来模拟递归的操作

    为什么我们会想到使用栈来模拟呢?

    因为递归实际上就是编译器将函数各参数放入递归内部,返回时再将其弹出,所以我们用一个栈来模拟递归操作,是再合适不过的。我们写一个循环判断栈中是否为空,为空则说明没有元素要处理了,那么循环内的逻辑就是先创立一个临时的节点指针指向当前栈口处元素,先判断其是否为空,为空不能操作,否则会操作空指针。
    不为空时我们先将遍历到的节点直接放入数组中,因为前序遍历是先处理中间节点,这之后我们按照先放入右节点再放入左节点的规律来使节点指针向后遍历。

    这是为什么呢?

    原因在于栈的独特定义,我们要先放入右节点再放入左节点,才能在下一步时候先处理左节点!以下代码

    class Solution 
    {
    public:
        vector<int> preorderTraversal(TreeNode* root) 
        {
            stack<TreeNode*> s;
            vector<int>result;
            if(root==nullptr)return result;
            s.push(root);//加入的是节点而并非节点对应的值,这里要尤其注意
            while(!s.empty())
            {
                TreeNode* node=s.top();
                s.pop();
                if(node)
                result.push_back(node->val);
                else continue;
                s.push(node->right);
                s.push(node->left);
            }
            return result;
        }
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    后序遍历

    后序遍历思路很类似,需要注意的是后序遍历的情况为”左右中“,我们可以按照前序遍历的代码模板将前序遍历的压栈操作改为先放入左节点再放入右节点,这样它对应的原本遍历方法是中右左,当全部遍历完成之后,我们将数组部分反转,即可得到后序遍历。

    class Solution {
    public:
        vector<int> postorderTraversal(TreeNode* root) {
            stack<TreeNode*>s;
            vector<int>result;
            if(root==nullptr)
            return result;
            s.push(root);
            while(!s.empty())
            {
                TreeNode*node=s.top();
                s.pop();
                if(node)
                result.push_back(node->val);
                else continue;
                s.push(node->left);
                s.push(node->right);
            }
            reverse(result.begin(),result.end());
            return result;
        }
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    这里可以看出,后序和前序的代码差别不大,也就是改了入栈的顺序,和反转了一下数组。

    中序遍历

    接着,我们再来看看中序遍历。

    中序遍历的思路是:我们需要建立一个指针来存储各节点,然后我们将它们放入栈内,我们先一直向二叉树的左节点遍历直到无法继续为止,弹出栈顶元素,此时栈顶元素即为我们要找的元素,为什么呢?

    因为我们是一直向左走,直到无法再向左走,弹出的那个元素,此时就是这个子树的中间节点(由于没有左节点)直接放入答案数组内,再遍历这个节点的右节点(不要忘记将其压入栈内),如果它有左节点接着遍历,如果它没有,那么就直接弹出,重复上述操作。

    为什么中序遍历不能像前后序那样只调整位置呢?
    拿前序遍历举例它是先遍历的中间节点也是先处理的中间节点,后序的代码前面思路也是如此,只是后面有调整位置,换句话说,是此时遍历的节点正是我们此时要处理的数据!

    那我们在遍历中序时候可以先遍历左边子树达成一样的逻辑吗?答案肯定是否定的,因为我们一开始只有root这个节点,而它指向了根节点,也就是说我们注定是先遍历中间节点,所以这样的方法并不适合中序迭代。

    class Solution {
    public:
        vector<int> inorderTraversal(TreeNode* root) {
            stack<TreeNode*>s;
            vector<int>result;
            TreeNode*cur=root;
            while(cur!=nullptr||!s.empty()){
                if(cur){
                    s.push(cur);cur=cur->left;
                }
                else{
                    cur=s.top();s.pop();
                    if(cur)result.push_back(cur->val);
                    cur=cur->right;
                }
            }
            return result;
        }
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    标记迭代法

    那么有没有可以将三种迭代统一起来的代码呢?也是有的,这里的思路也是创建一个栈来模拟,不同的是,我们将凡是已经遍历过的数据一股脑地放进去,在我们要处理的数据之后紧接着加入一个null来标记,这种思路也被称为标记迭代法。当我们遍历到null的时候,就对下一数据进行加入答案数组的处理。

    中序

    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;
        }
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

    该思路旨在if条件语句中处理压栈操作,else里处理加入答案数组,使代码变得简介,但是思路难想到,建议只记一种思路。

    总结:

    今天我们完成了树的递归遍历和非递归遍历,了解了一种新的方法标记迭代法,相关的思想需要多复习回顾。接下来,我们继续进行算法练习。希望我的文章和讲解能对大家的学习提供一些帮助。

    当然,本文仍有许多不足之处,欢迎各位小伙伴们随时私信交流、批评指正!我们下期见~

    在这里插入图片描述

  • 相关阅读:
    RabbitMQ部署指南
    人工智能 | ShowMeAI资讯日报 #2022.06.29
    vue3项目集成TypeScript
    05-树8 File Transfer(并查集)
    Go 定时任务执行-cron
    《算法导论》14.3 区间树
    【数据结构】优先级队列(堆)
    SwiftUI 中为什么应该经常用子视图替换父视图中的大段内容?
    05_css选择器的使用
    c语言 --- 分支结构
  • 原文地址:https://blog.csdn.net/m0_73258399/article/details/133556956