c++-二叉树进阶

文章目录

  • 前言
  • 一、二叉搜索树
    • 1、二叉搜索树介绍
    • 2、二叉搜索树循环实现
    • 3、二叉搜索树递归实现
    • 4、二叉搜索树的性能分析
    • 5、二叉搜索树的应用
    • 6、二叉树练习题
      • 6.1 根据二叉树创建字符串
      • 6.2 二叉树的层序遍历
      • 6.3 二叉树的层序遍历 II
      • 6.4 二叉树的最近公共祖先
      • 6.5 二叉搜索树与双向链表
      • 6.6 从前序与中序遍历序列构造二叉树
      • 6.7 从中序与后序遍历序列构造二叉树\
      • 6.8 二叉树的前序遍历--非递归
      • 6.9 二叉树的中序遍历--非递归
      • 6.10 二叉树的后序遍历


前言


一、二叉搜索树

1、二叉搜索树介绍

二叉搜索树(BST,Binary Search Tree),又称二叉排序树或二叉查找树,它或者是一棵空树,或者是具有以下性质的二叉树:
(1). 若它的左子树不为空,则左子树上所有节点的值都小于根节点的值。
(2). 若它的右子树不为空,则右子树上所有节点的值都大于根节点的值。
(3). 它的左右子树也分别为二叉搜索树。
在这里插入图片描述

2、二叉搜索树循环实现

下面我们来实现一个二叉搜索树。
我们先定义二叉搜索树的结点和类。
在这里插入图片描述
下面我们来实现二叉搜索树的插入,二叉搜索树的插入位置是非常确定的,默认的二叉搜索树是不允许数据冗余的,所以当插入相同的数据时会插入失败。
下面的实现是不行的,因为我们并没有将新结点链接到二叉搜索树中。
在这里插入图片描述
在这里插入图片描述
所以我们在找到新结点的插入位置之后,还需要知道这个位置的父结点,然后改变父结点的孩子,这样才能将新结点链接到二叉搜索树中。并且我们在创建新结点时会调用结构体BSTreeNode的构造函数,所以我们需要写出BSTreeNode的构造函数。
在这里插入图片描述
在这里插入图片描述
然后我们写一个递归版的中序遍历二叉树的函数来验证我们的搜索二叉树插入是否正确。但是我们在使用BSTree类实例化的对象调用InOrder方法时发现参数没法传递,因为我们需要传入二叉搜索树的根结点 _root,但是根节点 _root的访问权限为private,不可以在类外访问。并且因为使用递归的方法实现InOrder函数,所以必须要有形参。
在这里插入图片描述
在这里插入图片描述
上面的情况我们使用缺省值的话是不行的,因为缺省值必须是全局变量或者常量或者静态变量,需要生命周期和程序一样,即存储在静态区的变量,而 _root为局部成员变量。并且在将 _ root当作缺省值时,需要this指针,即其实是这样的Node * root = this-> _ root,但是在形参列表中this指针才刚定义,还不能使用,只有在函数内才可以使用this指针。所以实现缺省值的话是不行的。上面的情况我们一般有两种解决办法。
在这里插入图片描述
第一种办法:可以在类中定义一个GetRoot的方法,用来将 _root结点返回,我们看到这种办法成功的中序遍历了二叉搜索树。但是我们一般不推荐这种写法。
在这里插入图片描述
在这里插入图片描述
第二种办法:此时可以再嵌套一层,这样调用中序遍历就是无参的了,但是在底层调用了有参的。递归也使用的有参的。我们推荐这种写法,这样当BSTree类实例化的对象调用成员函数时比较统一。
在这里插入图片描述
在这里插入图片描述
我们看到了上面中序遍历二叉搜索树打印的结果为一个升序的数组,这也是二叉搜索树的特性之一,所以二叉搜索树又被称为二叉排序树。下面我们来实现二叉搜索树的查找函数。
在这里插入图片描述
下面再来实现二叉搜索树的删除.二叉搜索树的删除的情况很多,下面我们来看删除的最普遍的三种情况。
第1种情况删除的结点为叶子结点,我们直接将该结点删除即可,即将该结点的父结点的左孩子或右孩子指向nullptr。
第2种情况删除的结点有一个孩子结点,此时可以使用托孤法,即将该结点的孩子结点替代自己,让该结点的父结点的左孩子或右孩子指向该结点的孩子。
第3种情况删除的结点有两个孩子结点,此时删除该结点不能使用托孤法,因为两个孩子没办法都托孤给父结点,此时可以使用请保姆法,即将该结点的左子树最大结点或右子树最小结点选为保姆,然后将这两个结点的值换一下,此时对保姆结点使用托孤法,然后删除保姆结点,这样就达到了删除指定结点的效果。可以看到我们其实并没有删除指定结点,而是删除的和指定结点交换完值之后的保姆结点,这就是伪删法。
在这里插入图片描述
当分析了删除的一些情况后,下面我们就来使用代码实现。我们看到其实第1种方法和第2种方法都可以使用托孤,即删除结点的左为空让父结点指向删除结点的右孩子,删除结点的右为空让父节点指向删除结点的左孩子。
要删除该结点之前我们需要先找到该结点,如果找不到该结点就为删除失败。
在这里插入图片描述
当找到要删除的结点后,我们先将第1种情况和第2种情况的删除写好。因为前面我们分析了删除叶子结点也可以使用托孤法,所以我们就将删除叶子结点也使用托孤法来实现。
在这里插入图片描述
当要删除结点的左右结点都有孩子时,我们就需要使用请保姆法,但是因为保姆也可能有孩子,需要先进行托孤,所以我们需要记录保姆的父结点。我们需要找到要删除结点的左子树的最右结点或右子树的最左结点来作为保姆。然后将要删除结点的值和保姆结点的值互换,然后将保姆结点完成托孤,最后删除保姆结点即可。
在这里插入图片描述
在这里插入图片描述
但是我们上面写的代码有一个情况会出错。即当cur的右孩子就是cur的右子树的最左结点时,此时就不会进入while(minRight != nullptr)的循环中,那么此时pminRight就为nullptr,然后执行循环后面的pminRight->left == minRight时就会出现空指针错误。
在这里插入图片描述
在这里插入图片描述
所以我们不能将pminRight初始化为nullptr,而应该初始化为cur。
在这里插入图片描述
在这里插入图片描述
那么我们前面将parent = nullptr也会出现错误,即如果出现下面的情况就会出现错误。因为当要删除的结点为根节点,并且根节点的左子树或者右子树为空时,那么parent = nullptr,就会出现空指针异常。
在这里插入图片描述
上面的情况我们有两种解决办法,第一种解决办法就是找到根结点左子树最大值或右子树最小值来和根结点替换。
第二种解决办法就是将更新_root,即让根结点不为空的孩子结点更新为新的根结点。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

3、二叉搜索树递归实现

上面我们使用循环实现了二叉搜索树,下面我们使用递归来实现二叉搜索树。我们先使用递归来实现FindR函数。
在类里面实现的递归函数,一般都需要嵌套一层,我们用递归函数实现FindR函数也是一样。
在这里插入图片描述
然后我们用递归实现InsertR插入函数,当我们找到结点要插入的位置时,我们需要将key结点与父结点链接起来,但是因为适用了递归实现,所以现在找不到父结点。此时我们有三种解决办法。第一种方法可以再加一个参数,传递父结点。第二种方法不要判断root=nullptr,而是判断root的左右孩子是否为空,这样root就是父结点了。但是这两种方法都不是最好的方法。
在这里插入图片描述
第三种方法为最优方案,在第一个参数中加一个引用即可。即将当前递归函数的root为上一个递归函数的root的左孩子或右孩子的别名,那么在当前递归函数中就能通过别名来修改父结点的左孩子或右孩子的值。
在这里插入图片描述
在这里插入图片描述
下面我们再来递归实现删除。我们在实现递归删除是也将递归函数的第一个参数为Node * 类型的引用。
在这里插入图片描述
我们看到当删除的结点只有一个孩子或者没有孩子时,我们可以很好的删除,但是当删除的结点有两个孩子时,我们很难将这个结点删除。
在这里插入图片描述
此时我们需要找到删除结点左子树的最大值或者右子树的最小值来作为保姆,然后将要删除结点的值和保姆结点的值互换,然后将保姆结点删除即可。删除保姆结点时我们可以再一次递归删除,因为此时保姆结点只有一个结点或者没有结点,所以肯定会在前面两个if语句内被删除。需要注意的是调用递归传入的值一定要为root->left或root->right,不能是maxleft,因为maxleft为一个临时结点指针,改变maxleft的话并不会改变二叉搜索树里面的内容,所以我们需要传二叉搜索树中的结点。
在这里插入图片描述
在这里插入图片描述

下面我们来看二叉搜索树的拷贝,我们没有写二叉搜索树的拷贝构造函数时,此时适用编译器默认生成的拷贝构造函数,为浅拷贝。可以看到t1和t2指向同一棵二叉搜索树。但是我们发现程序并没有报错,这是因为我们还没有写析构函数,所以并不会报错。
在这里插入图片描述
我们先实现二叉搜索树的析构函数,我们需要使用后序遍历来删除每一个结点。即先删除左孩子,再删除右孩子,最后删除根节点。这样才能确保每一个结点都被删除。我们适用递归来实现析构函数,所以也将析构函数进行了一层嵌套,其实析构函数底层调用的Destroy函数来实现结点的删除。
在这里插入图片描述
我们还可以将Destroy函数的参数设为Node * & ,这样在Destroy函数中将结点delete之后再将结点置为nullptr,并且在析构函数里面就不需要将_root = nullptr了。
在这里插入图片描述
当实现了析构函数之后,适用浅拷贝的默认拷贝构造函数时就会出错,因为已经delete的二叉搜索树的结点已经置为nullptr了,再次delete时就会出错。
在这里插入图片描述
所以我们下面来实现二叉搜索树的深拷贝的拷贝构造函数。实现二叉搜索树的深拷贝,就需要再创建一棵新的二叉搜索树,但是我们不能复用Insert函数来创建新的二叉搜索树,因为二叉搜索树顺序不一样,得到的二叉搜索树也不一样。
在这里插入图片描述

下面我们使用递归来实现深拷贝,相当于先采用前序遍历的顺序创建结点,然后采用后序遍历的顺序链接结点。
在这里插入图片描述
当我们实现了拷贝构造函数后,因为拷贝构造函数也为构造函数,所以编译器就不会自动生成默认的构造函数了,此时我们创建t1对象时就会因为没有默认的构造函数而报错,此时我们可以写一个无参的默认构造函数,也可以使用default强制生成默认构造函数。
在这里插入图片描述
在这里插入图片描述
在下面测试拷贝构造函数中,我们看到t1和t2两棵二叉搜索树的根节点不同,则说明此时t1和t2为两棵二叉搜索树,即我们实现了深拷贝的拷贝构造函数。
在这里插入图片描述
下面为BSTree类的赋值运算符重载函数。这样我们就基本实现了BSTree类。
在这里插入图片描述
在这里插入图片描述

4、二叉搜索树的性能分析

二叉搜索树的插入和删除操作都必须先查找,查找效率代表了二叉搜索树中各个操作的性能。
对有n个结点的二叉搜索树,若每个元素查找的概率相等,则二叉搜索树平均查找长度是结点在二叉搜索树的深度的函数,即结点越深,则比较次数越多。但对于同一个关键码集合,如果各关键码插入的次序不同,可能得到不同结构的二叉搜索树。我们可以看到如果退化成单支树,二叉搜索树的性能就失去了。那能否进行改进,不论按照什么次序插
入关键码,二叉搜索树的性能都能达到最优?其实我们后面要学习的AVL树和红黑树就可以上场了。
在这里插入图片描述

5、二叉搜索树的应用

1.K模型:K模型即只有key作为关键码,结构中只需要存储Key即可,关键码即为需要搜索到的值。
比如:给一个单词word,判断该单词是否拼写正确,具体方式如下:
以词库中所有单词集合中的每个单词作为key,构建一棵二叉搜索树。
在二叉搜索树中检索该单词是否存在,存在则拼写正确,不存在则拼写错误。

2.KV模型:每一个关键码key,都有与之对应的值Value,即<Key, Value>的键值对。该种方式在现实生活中非常常见:
比如英汉词典就是英文与中文的对应关系,通过英文可以快速找到与其对应的中文,英文单词与其对应的中文<word, chinese>就构成一种键值对。
再比如统计单词次数,统计成功后,给定单词就可快速找到其出现的次数,单词与其出现次数就是<word, count>就构成一种键值对。

上面我们实现的二叉搜索树就是K模型版的,下面我们简单实现一个KV模型的二叉搜索树。
我们在向二叉搜索树中插入、删除、寻找结点时,还是按照key的值来进行寻找,只不过在二叉搜索树的结点中还有一个value可以存一个与key对应的值。
在这里插入图片描述
在这里插入图片描述
此时我们可以根据key的值从二叉搜索树中查找结点,然后打印出这个结点的value的值,这就相当于我们使用英语单词在二叉搜索树中查找,然后二叉搜索树的结点中存的有这个英语单词对应的中文意思,我们可以根据英语单词找到中文意思。
在这里插入图片描述
我们也可以创建一个二叉搜索树来存水果的个数,然后我们根据水果的名字就可以在二叉搜索树中找到水果的个数。并且因为我们将水果的名称作为key值了,所以二叉搜索树中的排序就是按照字符串的ASCII码来进行比较的。
在这里插入图片描述

6、二叉树练习题

6.1 根据二叉树创建字符串

题目链接
在这里插入图片描述
我们可以分析出共有下面的4种情况:
如果当前节点有两个孩子,那我们在递归时,需要在两个孩子的结果外都加上一层括号;
如果当前节点没有孩子,那我们不需要在节点后面加上任何括号;
在这里插入图片描述
如果当前节点只有左孩子,那我们在递归时,只需要在左孩子的结果外加上一层括号,而不需要给右孩子加上任何括号;
在这里插入图片描述
如果当前节点只有右孩子,那我们在递归时,需要先加上一层空的括号 ‘()’\text{`()'}‘()’ 表示左孩子为空,再对右孩子进行递归,并在结果外加上一层括号。
在这里插入图片描述
通过上面的分析我们可以写出代码如下,即直接按照分析的逻辑来写判断。

class Solution {
public:
    string tree2str(TreeNode* root) {
        string str;
        if(root == nullptr)
        {
            return str;
        }
        str=to_string(root->val);
        //左右孩子都不为空,则都加()
        if(root->left!=nullptr&&root->right!=nullptr)
        {
            str+='(';
            str+=tree2str(root->left);
            str+=')';
            str+='(';
            str+=tree2str(root->right);
            str+=')';
        }
        //左孩子为空,右孩子为空,那么都不加()
        else if(root->left==nullptr&&root->right==nullptr)
        {

        }
        //左孩子不为空,右孩子为空,只给左孩子加(),右孩子不需要加
        else if(root->left!=nullptr&&root->right==nullptr)
        {
            str+='(';
            str+=tree2str(root->left);
            str+=')';
        }
        //左孩子为空,右孩子不为空,则给左右孩子都加()
        else
        {
            str+='(';
            str+=tree2str(root->left);
            str+=')';
            str+='(';
            str+=tree2str(root->right);
            str+=')';
        }
        return str;
    }
};

但是上面的判断比较多,我们也可以使用下面的判断

class Solution {
public:
    string tree2str(TreeNode* root) {
        string str;
        if(root == nullptr)
        {
            return str;
        }
        str=to_string(root->val);
        //只有左右都为空的时候左边才不加(),所以我们可以使用下面的判断
        //只要不是左右都为空,那么左边就加()
        if(root->left!=nullptr || root->right!=nullptr)
        {
            str += '(';
            str += tree2str(root->left);
            str += ')';
        }
        //当左边不为空,右边为空时,右边才不加()。
        if(root->right!=nullptr)
        {
            str += '(';
            str += tree2str(root->right);
            str += ')';
        }
        return str;
    }
};

6.2 二叉树的层序遍历

题目链接
在这里插入图片描述
第一种方法:我们使用两个队列,一个队列记录层序遍历的结点,另一个队列记录结点所在层数。
在这里插入图片描述

class Solution {
public:
    vector<vector<int>> levelOrder(TreeNode* root) {
        vector<vector<int>> vv;
        //记录层序遍历结点
        queue<TreeNode*> s1;
        //记录结点所在的层数  
        queue<int> s2;
        //记录当前遍历的层数
        int count = 1;
        s1.push(root);
        s2.push(count);
        while(!s1.empty() && s1.front()!=nullptr)
        {
            vector<int> v;
            //如果记录结点层数的队列中还有当前层数的结点,那么继续遍历
            while(s2.front() == count)
            {
                TreeNode* front = s1.front();
                v.push_back(front->val);
                s1.pop();
                s2.pop();
                //如果当前结点有左右孩子,那么入栈,并且左右孩子的层数要+1.
                if(front->left!=nullptr)
                {
                    s1.push(front->left);
                    s2.push(count+1);
                }
                if(front->right!=nullptr)
                {
                    s1.push(front->right);
                    s2.push(count+1);
                }
            }
            count++;
            vv.push_back(v);
        }

        return vv;
    }
};

第二种方法:我们也可以使用一个队列来解决这一题,即使用一个队列来记录层序遍历的结点,然后使用levelSize记录这一层结点的个数即可。
在这里插入图片描述

class Solution {
public:
    vector<vector<int>> levelOrder(TreeNode* root) {
        queue<TreeNode*> s1;
        if(root!=nullptr)
        {
            s1.push(root);
        }
        int levelSize = 1;
        vector<vector<int>> vv;
        while(!s1.empty())
        {
            vector<int> v;
            while(levelSize)
            {
                TreeNode* front = s1.front();
                v.push_back(front->val);
                s1.pop();
                levelSize--;
                if(front->left!=nullptr)
                {
                    s1.push(front->left);
                }
                if(front->right!=nullptr)
                {
                    s1.push(front->right);
                }
            }
            levelSize=s1.size();
            vv.push_back(v);
        }

        return vv;
    }
};

6.3 二叉树的层序遍历 II

题目链接
在这里插入图片描述
这一题我们可以使用一个巧的办法,即将上一题的结果使用reverse函数反转以下即可。

class Solution {
public:
    vector<vector<int>> levelOrderBottom(TreeNode* root) {
queue<TreeNode*> s1;
        if(root!=nullptr)
        {
            s1.push(root);
        }
        int levelSize = 1;
        vector<vector<int>> vv;
        while(!s1.empty())
        {
            vector<int> v;
            while(levelSize)
            {
                TreeNode* front = s1.front();
                v.push_back(front->val);
                s1.pop();
                levelSize--;
                if(front->left!=nullptr)
                {
                    s1.push(front->left);
                }
                if(front->right!=nullptr)
                {
                    s1.push(front->right);
                }
            }
            levelSize=s1.size();
            vv.push_back(v);
        }

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

6.4 二叉树的最近公共祖先

题目链接
在这里插入图片描述
这个题我们看到它的结点中没有记录给父结点,如果是三叉链,即记录父结点的话,那么可以将这个题转换为链表相交问题了。但是这个题的结点没有提供父结点,那么我们就需要先找一下规律。
我们看到如果一个孩子在我的左子树,一个孩子在我的右子树,那么我就是公共祖先。而第三种情况我们需要特殊判断,如果我的右子树或者左子树包含一个结点,那么我就是公共祖先。
在这里插入图片描述

class Solution {
public:
    bool IsInTree(TreeNode* node,TreeNode* key)
    {
        if(node == nullptr)
        {
            return false;
        }
        if(node == key)
        {
            return true;
        }
        return IsInTree(node->left,key) || IsInTree(node->right,key);
    }
    TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
        if(root == nullptr)
        {
            return nullptr;
        }
        //p或q是根,另一个是孩子,root就是最近公共祖先
        if(p == root || q == root)
        {
            return root;
        }

        bool pInLeft = IsInTree(root->left,p);
        bool pInRight = !pInLeft;
        
        bool qInLeft = IsInTree(root->left,q);
        bool qInRight = !qInLeft;

        //如果一个在左,一个在右,则当前结点就是公共祖先
        if((pInLeft && qInRight) || (qInLeft && pInRight))
        {
            return root;
        }
        //如果两个都在左,就去当前结点的左子树找
        else if(pInLeft && qInLeft)
        {
            return lowestCommonAncestor(root->left,p,q);
        }
        //如果两个都在右,就去当前结点的右子树找
        else
        {
            return lowestCommonAncestor(root->right,p,q);
        }
    }
};

上面的写法的时间复杂度为O(N^2),因为每一次查找结点是否在左子树或右子树为O(N),因为二叉树可能为不平衡二叉树,这样极端情况下就需要查N次。即向下面的情况下,是效率最低的情况。
在这里插入图片描述
如果这个树是二叉搜索树的话,上面的方法就可以改变判断做到时间复杂度为O(N),因为只需要比较p、q是否比根节点大或小,如果比根小,递归左树查找,如果比根大,递归右树查找。但是题目中的树不是二叉搜索树,我们此时想要优化为O(N)的话,可以使用DFS深度优先遍历,然后将p和q的路径都存放到两个栈中,转换为p和q的路径相交问题。

lass Solution {
public:
    bool GetPath(TreeNode* root, TreeNode* x, stack<TreeNode*>& path)
    {
        if(root == nullptr)
        {
            return false;
        }
        path.push(root);
        if(root == x)
        {
            return true;
        }
        if(GetPath(root->left,x,path))
        {
            return true;
        }
        if(GetPath(root->right,x,path))
        {
            return true;
        }
        path.pop();
        return false;
    }
    TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
        stack<TreeNode*> pPath,qPath;
        //找到p和q的路径
        GetPath(root,p,pPath);
        GetPath(root,q,qPath);
        while(pPath.size()!=qPath.size())
        {
            if(pPath.size()>qPath.size())
            {
                pPath.pop();
            }
            else
            {
                qPath.pop();
            }
        }
        while(pPath.top()!=qPath.top())
        {
            pPath.pop();
            qPath.pop();
        }

        return pPath.top();
    }
};

6.5 二叉搜索树与双向链表

题目链接
在这里插入图片描述
例如下面的一棵二叉树,我们想要将这棵二叉树变为双向链表,我们需要将每一个结点的left指向它的上一个结点,上一个结点的right指向这一个结点,如果我们使用递归并且将递归函数的参数为指针的话,我们知道在当前的递归函数中没法改变prev结点的right和left,这时我们可以将参数prev设为指针引用,那么我们在当前递归函数中也可以改变当前结点的上一个结点prev的left和right的值。
在这里插入图片描述

class Solution {
public:
	void InOrderConvert(TreeNode* cur,TreeNode*& prev)
	{
		if(cur == nullptr)
		{
			return;
		}
		//二叉搜索树中序遍历为升序
		InOrderConvert(cur->left, prev);
		//当前结点的left指向上一个结点
		cur->left = prev;
		//如果上一个结点不为空,即cur不为链表头结点,那么就将上一个结点的right指向当前结点cur。
		if(prev)
		{
			prev->right = cur;
		}
		//此时将prev变为当前结点,然后再链接cur->right。
		prev=cur;
		InOrderConvert(cur->right, prev);
	}

    TreeNode* Convert(TreeNode* pRootOfTree) {
        TreeNode* prev=nullptr;
		InOrderConvert(pRootOfTree, prev);
		TreeNode* head = pRootOfTree;
		while(head!=nullptr&&head->left!=nullptr)
		{
			head =head->left;
		}
		return head;
    }
};

6.6 从前序与中序遍历序列构造二叉树

题目链接
在这里插入图片描述
使用前序序列来确定根结点,使用中序序列来分割出根结点的左右子树区间。

class Solution {
public:
    TreeNode* _buildTree(vector<int>& preorder, vector<int>& inorder,int& prei,int inbegin,int inend)
    {
        if(inbegin>inend)
        {
            return nullptr;
        }
        TreeNode* root = new TreeNode(preorder[prei]);
        int rooti = inbegin;
        while(rooti<=inend)
        {
            if(inorder[rooti]==preorder[prei])
            {
                break;
            }
            else
            {
                rooti++;
            }
        }
        prei++;
        root->left=_buildTree(preorder,inorder,prei,inbegin,rooti-1);
        root->right=_buildTree(preorder,inorder,prei,rooti+1,inend);
        return root;
    }
    TreeNode* buildTree(vector<int>& preorder, vector<int>& inorder) {
        int i = 0;
        return _buildTree(preorder,inorder,i,0,inorder.size()-1);
    }
};

6.7 从中序与后序遍历序列构造二叉树\

题目链接
在这里插入图片描述
根据前序序列和中序序列生成二叉树时,我们是从左子树开始链接,而因为后序序列中根结点在最后一个,并且倒着访问后序序列时,先访问的都是右结点,所以我们采用从右子树开始链接。

class Solution {
public:
    TreeNode* _buildTree(vector<int>& inorder, vector<int>& postorder,int& posti,int inbegin,int inend)
    {
        if(inbegin>inend)
        {
            return nullptr;
        }
        TreeNode* root = new TreeNode(postorder[posti]);
        int rooti = inbegin;
        while(rooti<=inend)
        {
            if(inorder[rooti]==postorder[posti])
            {
                break;
            }
            else
            {
                rooti++;
            }
        }
        posti--;
        root->right = _buildTree(inorder,postorder,posti,rooti+1,inend);
        root->left= _buildTree(inorder,postorder,posti,inbegin,rooti-1);
        return root;
    }
    TreeNode* buildTree(vector<int>& inorder, vector<int>& postorder) {
        int i = postorder.size()-1;
        return _buildTree(inorder,postorder,i,0,i);
    }
};

6.8 二叉树的前序遍历–非递归

题目链接
在这里插入图片描述
我们使用非递归前序遍历二叉树时,可以先访问这棵树的左路结点,然后左路结点遇到空后,开始访问这个左路结点的右子树;访问右子树时就转换为子问题,即访问右子树也是先从右子树的左路结点开始访问,当右子树的左路结点遇到空时再重复上面的步骤。
在这里插入图片描述

class Solution {
public:
    vector<int> preorderTraversal(TreeNode* root) {
        vector<int> vv;
        stack<TreeNode*> s;
        TreeNode* cur = root;
        //
        while(cur!=nullptr || !s.empty())
        {
            //开始访问一棵树
            //1.先访问左路结点,
            //2.左路结点遇到空后,就开始访问左路结点的右子树
            while(cur!=nullptr)
            {
                vv.push_back(cur->val);
                s.push(cur);
                cur=cur->left;
            }
            //左路结点为空了,开始访问右子树
            //访问右子树也是像上面一样,先从右子树的左路结点开始访问。
            cur = s.top()->right;
            s.pop();
        }
        return vv;
    }
};

6.9 二叉树的中序遍历–非递归

题目链接
在这里插入图片描述
二叉树的中序遍历非递归实现和上面的前序遍历类似,只不过将元素插入到容器的时机不同。而非递归遍历二叉树的方式是一样的,都是先遍历这棵树的左路结点,然后左路结点为空后,再遍历这个左路结点的右子树。

class Solution {
public:
    vector<int> inorderTraversal(TreeNode* root) {
        vector<int> vv;
        stack<TreeNode*> s;
        TreeNode* cur = root;
        //先访问一棵树的左路结点,当左路结点为空时,再访问该结点的右子树
        //而访问右子树时也是先访问右子树的左路结点。
        while(cur!=nullptr || !s.empty())
        {
            while(cur!=nullptr)
            {
                s.push(cur);
                cur=cur->left;
            }
            //因为是中序遍历,所以当左路结点没有左孩子时,此时才能访问这个结点。
            vv.push_back(s.top()->val);
            cur = s.top()->right;
            s.pop();
        }

        return vv;
    }
};

6.10 二叉树的后序遍历

题目链接
在这里插入图片描述
二叉树的后序遍历的非递归实现和前序遍历、中序遍历不同。因为有左右子树的结点会有两次都在栈顶的位置,我们按照上面前序遍历的写法无法区分哪一次将该结点进行出栈,所以我们在代码中加了一个prev指针,prev指针记录上一个栈顶结点,如果当前栈顶结点的右孩子为prev结点,说明当前栈顶结点的左右子树都已经遍历完毕,此时就需要将栈顶元素进行出栈了。
在这里插入图片描述

class Solution {
public:
    vector<int> postorderTraversal(TreeNode* root) {
        vector<int> vv;
        stack<TreeNode*> s;
        TreeNode* cur = root;
        TreeNode* prev = nullptr;
        while(cur!=nullptr || !s.empty())
        {
            while(cur!=nullptr)
            {
                s.push(cur);
                cur=cur->left;
            }
            TreeNode* top = s.top();
            if((top->right==nullptr) || (top->right == prev))
            {
                vv.push_back(top->val);
                s.pop();
                prev = top;
            }
            else
            {
                cur=top->right;
            }
        }
        return vv;
    }
};

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/109411.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Java SE 学习笔记(十八)—— 注解、动态代理

目录 1 注解1.1 注解概述1.2 自定义注解1.3 元注解1.4 注解解析1.5 注解应用于 junit 框架 2 动态代理2.1 问题引入2.2 动态代理实现 1 注解 1.1 注解概述 Java 注解&#xff08;Annotation&#xff09;又称Java标注&#xff0c;是JDK 5.0引入的一种注释机制&#xff0c;Java语…

二叉排序树c语言版

1、定义二叉树数据域、二叉树结点 /*** 二叉树节点数据 */ typedef struct treenodedata {int sort;char* name;} TreeNodeData;/**** 二叉树节点定义 */ typedef struct binarytree {/*** 结点数据域*/TreeNodeData* data;/**左子树*/struct binarytree* leftChild;/**左子树…

【C语言】指针那些事(上)

C语言系列 文章目录 文章目录 一. 字符指针 一.&#xff08;1 &#xff09; 数组创建空间的地址和指针指向的地址 二. 指针数组 二.&#xff08;1&#xff09;指针数组模拟一个二维数组 ​ 三. 数组指针 三.(1)数组指针到底有什么用 对一维数组没有什么用 二.(…

半导体产线应用Power Link 转EtherCAT协议网关数字化转型

随着数字化转型的推进&#xff0c;越来越多的企业开始意识到数字化转型的重要性&#xff0c;并将其作为发展战略的关键之一。半导体产线作为一个高度自动化的生产系统&#xff0c;自然也需要数字化转型来提高效率、降低成本和提高质量。Power Link 转EtherCAT协议网关是半导体产…

大数据之LibrA数据库系统告警处理(ALM-12002 HA资源异常)

告警解释 HA软件周期性检测Manager的WebService浮动IP地址和数据库。当HA软件检测到浮动IP地址或数据库异常时&#xff0c;产生该告警。 当HA检测到浮动IP地址或数据库正常后&#xff0c;告警恢复。 告警属性 告警参数 对系统的影响 如果Manager的WebService浮动IP地址异常…

高效分割分段视频:提升您的视频剪辑能力

在数字媒体时代&#xff0c;视频剪辑已经成为一项重要的技能。无论是制作个人影片、广告还是其他类型的视频内容&#xff0c;掌握高效的视频剪辑技巧都是必不可少的。本文将介绍如何引用云炫AI智剪高效地分割和分段视频&#xff0c;以提升您的视频剪辑能力。以下是详细的操作步…

时序预测 | Matlab实现ARIMA-LSTM差分自回归移动差分自回归移动平均模型模型结合长短期记忆神经网络时间序列预测

时序预测 | Matlab实现ARIMA-LSTM差分自回归移动差分自回归移动平均模型模型结合长短期记忆神经网络时间序列预测 目录 时序预测 | Matlab实现ARIMA-LSTM差分自回归移动差分自回归移动平均模型模型结合长短期记忆神经网络时间序列预测预测效果基本介绍程序设计参考资料 预测效果…

【设计模式】第19节:行为型模式之“中介模式”

一、简介 中介模式定义了一个单独的&#xff08;中介&#xff09;对象&#xff0c;来封装一组对象之间的交互。将这组对象之间的交互委派给与中介对象交互&#xff0c;来避免对象之间的直接交互。 中介模式的设计思想跟中间层很像&#xff0c;通过引入中介这个中间层&#xf…

【Java】LinkedList 集合

LinkedList集合特点 LinkedList 底层基于双向链表实现增删 效率非常高&#xff0c;查询效率非常低。 LinkedList源码解读分析 LinkedList 是双向链表实现的 ListLinkedList 是非线程安全的&#xff08;线程是不安全的&#xff09;LinkedList 元素允许为null,允许重复元素Linked…

NewStarCTF2023week4-midsql(利用二分查找实现时间盲注攻击)

大致测试一下&#xff0c;发现空格被过滤了 使用内联注释/**/绕过&#xff0c;可行 1/**/-- 使用%a0替代空格&#xff0c;也可以 1%a0-- 再次测试发现等号也被过滤&#xff0c;我们使用 like 代替 &#xff08;我最开始以为是and被过滤&#xff0c;并没有&#xff0c;如果是…

大数据之LibrA数据库系统告警处理(ALM-12001 审计日志转储失败)

告警解释 根据本地历史数据备份策略&#xff0c;集群的审计日志需要转储到第三方服务器上。如果转储服务器满足配置条件&#xff0c;审计日志可以成功转储。审计日志转储失败&#xff0c;系统产生此告警。如果第三方服务器的转储目录磁盘空间不足&#xff0c;或者用户修改了转…

力扣第968题 监控二叉树 c++ hard题 二叉树的后序遍历 + 模拟 + 贪心

题目 968. 监控二叉树 困难 相关标签 树 深度优先搜索 动态规划 二叉树 给定一个二叉树&#xff0c;我们在树的节点上安装摄像头。 节点上的每个摄影头都可以监视其父对象、自身及其直接子对象。 计算监控树的所有节点所需的最小摄像头数量。 示例 1&#xff1a; …

机器人制作开源方案 | 大学宿舍蓝牙遥控水卡机

一些看起来不太聪明的机器到底是用来干什么的&#xff1f; 用来解决一些不太聪明的基础设施。 想必大家都见过一些奇葩反人类的——设计&#xff0c;举例如下&#xff1a; 当我们一边对着这些图片狂笑时…… 有没有想过“报应”有一天会落在自己身上呢&#xff1f; 这天我们遇到…

Android开发知识学习——HTTP基础

文章目录 学习资源来自&#xff1a;扔物线HTTPHTTP到底是什么HTTP的工作方式URL ->HTTP报文List itemHTTP的工作方式请求报文格式&#xff1a;Request响应报文格式&#xff1a;ResponseHTTP的请求方法状态码 HeaderHostContent-TypeContent-LengthTransfer: chunked (分块传…

IDEA 如何运行 SpringBoot 项目

步骤一&#xff1a;配置 Maven 第一步&#xff1a;用 IDEA 打开项目&#xff0c;准备配置 maven 环境 &#xff0c;当然如果本地没有提前配置好 maven&#xff0c;就用 IDEA 默认的配置即可 配置 maven 步骤 情况 1&#xff1a;如果本地没有配置过 maven&#xff0c;可以保持如…

Cesium加载GeoServer发布WMTS系列问题

这两天为了摆脱对ArcGIS的依赖&#xff0c;准备走开源这一套&#xff08;现在发布只能用这两个吗&#xff0c;知道的探讨一下&#xff09;&#xff0c;想走GeoServer的路线&#xff0c;在发布了shp之类的之后发现还挺好用的&#xff0c;但是到了发布影像的时候&#xff0c;就很…

【C++项目】高并发内存池第五讲内存回收释放过程介绍

内存回收 1.ThreadCache2.CentralCache3.PageCache 项目源代码&#xff1a;高并发内存池 1.ThreadCache void ThreadCache::Deallocate(void* ptr, size_t size) {assert(ptr);assert(size < MAX_BYTES);//计算在哪号桶中&#xff0c;然后插入进去size_t index SizeClass…

ZYNQ连载07-PIN设备

ZYNQ连载07-PIN设备 1. 简述 RT-Thread PIN设备 这里参看RT-Thread提供的PIN设备管理接口&#xff0c;简单封装了几个接口函数。 2. 实现 #include "include/drv_gpio.h" #define LOG_TAG "drv_gpio" static XGpioPs xgpiops;void rt_pin_mode(rt_…

SIT1028Q内置高压 LDO 本地互联网络(LIN)收发器

SIT1028Q 是一款内部集成高压 LDO 稳压源的本地互联网络&#xff08; LIN &#xff09;物理层收发器&#xff0c;可为外 部 ECU &#xff08; Electronic Control Unit &#xff09;微控制器或相关外设提供稳定的 5V/3.3V 电源&#xff0c;该 LIN 收发器 符合 LIN 2…

歌曲怎么转换为mp3格式播放

MP3 文件&#xff08;以 .mp3 文件扩展名结尾&#xff09;因其高品质、文件小和兼容性强成为最流行的音频文件类型之一&#xff0c;如果你正在寻找好用的音频转换工具&#xff0c;想将歌曲转换为 MP3 以便在任何设备上播放&#xff0c;本文将会为你推荐2款好用的音频转换工具&a…
最新文章