c++-二叉树进阶

news2025/1/14 18:18:11

文章目录

  • 前言
  • 一、二叉搜索树
    • 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.coloradmin.cn/o/1149365.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

项目文章 | Plant Physiology 华南农业大学揭示组蛋白修饰调节水稻器官大小的表观遗传机制

发表单位&#xff1a;华南农业大学生命科学学院 发表日期&#xff1a;2023年10月24日 期刊&#xff1a;Plant Physiology 影响因子&#xff1a;7.4 2023年10月24日&#xff0c;华南农业大学生命科学学院郑少燕副教授/刘振兰教授团队在Plant Physiology上发表题为“SET DOMA…

系列三十二、代理(二)静态代理

一、静态代理 1.1、概述 静态代理是由程序员创建或者工具生成代理类的源码&#xff0c;再编译代理类。在程序运行前就已经存在代理类的字节码文件了&#xff0c;即代理类和被代理类的关系在运行前就确定了。 简单理解&#xff1a;在程序运行前&#xff0c;代理类就存在了&…

Linux虚拟机安装centos7配置及常用命令

一.VMWare的安装 详情请看上期&#xff1a;VM虚拟机的安装与配置及操作系统的安装-CSDN博客 二.centos7安装 1.创建新的虚拟机 选择自定义&#xff0c;然后下一步 默认选择就行&#xff0c;直接下一步 选择稍后安装&#xff0c;然后直接下一步 选择Linux&#xff0c;然后向上…

【Python 零基础入门】常用内置函数 再探

【Python 零基础入门】内容补充 1 常用内置函数 Python 简介为什么要学习内置函数集合操作len(): 计算长度sorted(): 排序all(): 检查所有元素any(): 检查任一元素filter(): 过滤元素map(): 应用函数zip(): 组合元素 文件操作和输入输出open(): 打开文件read(): 读取文件write(…

深入解析 Spring Framework 中 @Autowired 注解的实现原理

摘要 关于Autowired注解的作用 Autowired 注解在Spring中的作用是实现依赖注入&#xff08;Dependency Injection&#xff09;&#xff0c;它用于自动装配&#xff08;autowiring&#xff09;Spring Bean 的依赖关系。具体来说&#xff0c; Autowired 注解有以下作用&#xf…

3D模拟场景开发引擎

在3D工程模拟开发中&#xff0c;有一些专门的引擎和工具可供选择&#xff0c;以帮助您创建逼真的三维模拟和模型。以下是一些用于3D工程模拟的开发引擎和工具&#xff0c;希望对大家有所帮助。北京木奇移动技术有限公司&#xff0c;专业的软件外包开发公司&#xff0c;欢迎交流…

Youtube DNN:Deep Neural Networks for YouTube Recommendations

1.介绍 本文主要解决的三个挑战&#xff1a; 大规模的推荐场景&#xff0c;能够支持分布式训练和提供有效率的服务。不断更新的新物料。稀疏的用户行为&#xff0c;包含大量的噪声。 2.推荐系统 文章包含推荐系统的两阶段模型&#xff1a;召回和排序。 召回网络根据用户的历…

CSS基础入门04

目录 1.内边距 1.1基础写法 1.2复合写法 2.外边距 2.1基础写法 2.2复合写法 2.3块级元素水平居中 3.去除浏览器默认样式 4.弹性布局 4.1初体验 5.flex 布局基本概念 6.常用属性 6.1justify-content 6.2align-items 1.内边距 padding 设置内容和边框之间的距离. …

Go 语言gin框架的web

节省时间与精力&#xff0c;更高效地打造稳定可靠的Web项目&#xff1a;基于Go语言和Gin框架的完善Web项目骨架。无需从零开始&#xff0c;直接利用这个骨架&#xff0c;快速搭建一个功能齐全、性能优异的Web应用。充分发挥Go语言和Gin框架的优势&#xff0c;轻松处理高并发、大…

Pycharm使用阿里云SDK发送短信(超详细+避坑!!)

文章目录 1. 注册阿里云2. 创建签名3. 创建模板4. 创建AccessKey5. API发送测试6. 获取示例7. pycharm发送验证码7.1 安装sdk7.2 参数修改7.3 发送随机四位数字7.4 验证码信息返回7.5 简易版代码&#xff1a; 1. 注册阿里云 访问阿里云官网&#xff0c;进行注册 https://www.…

ZYNQ连载03-Vivado创建工程

ZYNQ连载03-Vivado创建工程 1. 硬件参数 名称参数主控xc7z020clg400-2DDRMT41J256M16RE-125 2. 创建工程 3. 串口配置 4. DDR配置 5. SD配置 6. ETH配置 7. USB配置 8. 导出硬件 Generate Output ProductsCreate HDL WrapperExport Hardware Platform 执行以上步骤后&#…

华为eNSP cli(命令行界面) Courier New字体设置

想让eNSP cli&#xff08;命令行界面&#xff09;字体设置为Courier New时&#xff0c;但在字体设置界面找不到Courier New字体&#xff0c;如下图所示&#xff1a; 遇到上述问题的解决办法如下&#xff1a; 到C:\Windows\Fonts目录下查找courier new字体&#xff0c;如果找到…

EDA常用数字器件硬件描述

EDA常用数字器件硬件描述 前言 在使用了一段时间EDA编程之后&#xff0c;来回顾一下基本的知识&#xff0c;看看如何实现基本的EDA常用数字器件对应的硬件描述 一、组合逻辑器件描述 1. 基本的逻辑门电路 与、或、非&#xff08;取反&#xff09;、与非、或非、异或、同或 …

【每日一题】59. 螺旋矩阵 II

给你一个正整数 n &#xff0c;生成一个包含 1 到 n2 所有元素&#xff0c;且元素按顺时针顺序螺旋排列的 n x n 正方形矩阵 matrix 。 示例 1&#xff1a; 输入&#xff1a;n 3 输出&#xff1a;[[1,2,3],[8,9,4],[7,6,5]]示例 2&#xff1a; 输入&#xff1a;n 1 输出&…

软信天成:数据质量管理对企业有什么意义?

在这个信息爆炸的时代&#xff0c;数据已经成为了企业决策的基础&#xff0c;是企业成功的关键要素。然而&#xff0c;如果企业所获取的数据质量不佳&#xff0c;会对企业产生何种影响呢&#xff1f; 事实上&#xff0c;有效而准确的数据可以揭示出潜在的业务机遇&#xff0c;…

vue源码笔记之——运行时runtime

源码中的位运算 按位于 运算 if (shapeFlag & ShapeFlags.TELEPORT) {解释&#xff1a;如果shapFlag本身值为8&#xff0c;type为1的话&#xff0c;那么转换为二进制&#xff08;js都是32位&#xff09;那就是 shapFlag&#xff1a;00000000 00000000 00000000 00001000 …

6590亿美刀!美自食高息恶果,净利息支出即将超越军费

号外&#xff1a;教链内参10.28《普通人从0到1000万&#xff08;三千字&#xff09;》 当美经济分析局&#xff08;BEA&#xff09;日前给出三季度美国GDP初估增速高达与我国旗鼓相当的4.9%时&#xff0c;很多人欢欣鼓舞&#xff0c;全然不假思索&#xff0c;2007-2022年发电量…

SpringBoot中CommandLineRunner详解(含源码)

文章目录 前言实例导入库application.yamlRunnerSpringBootCommandLineRunnerApplication执行结果 先后顺序示例OrderRunner1OrderRunner2执行结果 通常用法加载初始化数据示例 启动后打印应用信息示例 启动异步任务示例 接口健康检查示例 外部服务调用示例 参数校验示例 动态设…

OpenCV学习笔记

OpenCV基础 threshold函数的使用 https://blog.csdn.net/u012566751/article/details/77046445 图像的二值化就是将图像上的像素点的灰度值设置为0或255&#xff0c;这样将使整个图像呈现出明显的黑白效果。在数字图像处理中&#xff0c;二值图像占有非常重要的地位&#xff0…

四个制作PPT的小技巧

制作PPT已经很麻烦了&#xff0c;学习一些小技巧可以帮助我们省时省力吧&#xff01; 技巧一&#xff1a;自动更新日期和时间 当我们给幻灯片添加了页脚并且是时间日期&#xff0c;可以通过设置达到自动更新&#xff0c;这样我们就不需要每次修改的时候都要手动更新日期和时间…