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.mzph.cn/news/129192.shtml

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

相关文章

ElasticSearch搜索技术深入与聚合查询实战

ES分词器详解 基本概念 分词器官方称之为文本分析器&#xff0c;顾名思义&#xff0c;是对文本进行分析处理的一种手段&#xff0c;基本处理逻辑为按照预先制定的分词规则&#xff0c;把原始文档分割成若干更小粒度的词项&#xff0c;粒度大小取决于分词器规则。 分词发生时…

R语言绘图-5-条形图(修改坐标轴以及图例等)

0. 说明&#xff1a; 1. 绘制条形图&#xff1b; 2. 添加文本并调整位置&#xff1b; 3. 调整x轴刻度的字体、角度及颜色&#xff1b; 4. 在导出pdf时&#xff0c;如果没有字体&#xff0c;该怎么解决问题&#xff1b; 1. 结果&#xff1a; 2. 代码&#xff1a; library(ggp…

文本批量处理,一键转换HTML文件编码,释放您的繁琐工作!

亲爱的用户&#xff0c;您是否曾经为需要手动转换HTML文件编码而耗费大量时间和精力而感到困扰&#xff1f;现在&#xff0c;我们为您提供了一款强大的文本批量处理工具&#xff01;让您一键将HTML文件编码进行转换&#xff0c;轻松释放您的繁琐工作&#xff01; 首先&#xf…

【带货案例】从美区十月带货达人身上寻找商品爆款秘诀!

2023只剩下最后两个月&#xff0c;年底也是各大商家冲刺卖货的黄金时期&#xff01; 带货过程中的一个重要环节即【达人营销】&#xff0c;背受跨境卖家关注。 下面选取美区十月带货达人TOP3&#xff0c;分析其带货秘诀。 据超店有数达人榜单显示&#xff1a;美区十月带货达人…

Qt中正确的设置窗体的背景图片的几种方式

Qt中正确的设置窗体的背景图片的几种方式 QLabel加载图片方式之一Chapter1 Qt中正确的设置窗体的背景图片的几种方式一、利用styleSheet设置窗体的背景图片 Chapter2 Qt的主窗口背景设置方法一&#xff1a;最简单的方式是通过ui界面来设置&#xff0c;例如设置背景图片方法二 &…

leetCode 198.打家劫舍 动态规划入门:从记忆化搜索到递推

leetCode 198.打家劫舍 198. 打家劫舍 - 力扣&#xff08;LeetCode&#xff09; 你是一个专业的小偷&#xff0c;计划偷窃沿街的房屋。每间房内都藏有一定的现金&#xff0c;影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统&#xff0c;如果两间相邻的房屋在同一…

LuaHttp库写的一个简单的爬虫

LuaHttp库是一个基于Lua语言的HTTP客户端库&#xff0c;可以用于爬取网站数据。与Python的Scrapy框架类似&#xff0c;LuaHttp库也可以实现网站数据的抓取&#xff0c;并且可以将抓取到的数据保存到数据库中。不过需要注意的是&#xff0c;LuaHttp库并不像Scrapy框架那样具有完…

3 数据库系统核心知识点

一、事务 先回顾一下事务的概念&#xff1a; 事务指的是满足 ACID 特性的一组操作&#xff0c;可以通过 Commit 提交一个事务&#xff0c;也可以使用 Rollback 进行回滚ACID 1. 原子性(Atomicity) 1.事务被视为不可分割的最小单元&#xff0c;事务的所有操作要么全部提交成…

pytorch dropout 置零 + 补偿性放缩

一句话概括&#xff1a;&#xff08;训练过程中&#xff09;Dropout 操作 随机置零 非置零元素进行后补偿性放缩。以保证dropout前后数据scale不变。 详细解释(来自chatgpt): 在 PyTorch 中&#xff0c;dropout 的操作不仅仅是将某些元素置零。为了确保期望输出在训练和测试…

Java NIO 编程

1. 简介 Java NIO 是 JDK 1.4 中引入的新的 IO 方式&#xff0c;它主要包含 Buffer、Channel、Selector 这三个核心的组件&#xff0c;它与传统 IO 的区别如下&#xff1a; NIO IO 面向缓冲 面向流 同步非阻塞 同步阻塞 多路复用&#xff08;选择器&#xff09; 无 1.1…

php语言

文章目录 常用命令Apache命令php命令 常用命令 Apache命令 查看版本号 D:\cjm\soft-work\phpStudy\phpstudy_pro\Extensions\Apache2.4.39\bin>httpd -v检测运行环境 httpd -tphp命令 PHP版本号 需要进入php目录 php -v

人工智能-深度学习计算:层和块

我们关注的是具有单一输出的线性模型。 在这里&#xff0c;整个模型只有一个输出。 注意&#xff0c;单个神经网络 &#xff08;1&#xff09;接受一些输入&#xff1b; &#xff08;2&#xff09;生成相应的标量输出&#xff1b; &#xff08;3&#xff09;具有一组相关 参数…

我的创作纪念日--AI小怪兽打怪进阶路

目录 自我介绍 时间轴 收获 日常 成就 憧憬 自我介绍 希望您持续关注AI小怪兽的不断进化、打怪&#xff01;&#xff01;&#xff01; AI小怪兽&#xff1a;1&#xff09;YOLO骨灰级玩家&#xff0c;YOLOv5、v7、v8优化创新&#xff0c;复现计算机视觉顶会&#xff0c;…

k8s的概念

概念 它是一种开源的容器编排平台&#xff0c;用于自动化部署&#xff0c;扩展和管理容器化的应用程序&#xff0c;它提供了一种容器编排和管理的方式&#xff0c;可以帮助开发人员更轻松地管理容器化的的应用程序&#xff0c;并且提供了一种跨多个主机的自动化部署和管理机制…

[python 刷题] 2866 Beautiful Towers II

[python 刷题] 2866 Beautiful Towers II 题目如下&#xff1a; You are given a 0-indexed array maxHeights of n integers. You are tasked with building n towers in the coordinate line. The ith tower is built at coordinate i and has a height of heights[i]. A co…

Win YAPI + Jenkins 实现接口自动化测试

自动化测试 传统的接口自动化测试成本高&#xff0c;大量的项目没有使用自动化测试保证接口的质量&#xff0c;仅仅依靠手动测试&#xff0c;是非常不可靠和容易出错的。 为了解决这个问题&#xff0c;使用YAPI接口自动化测试功能&#xff0c;只需要配置每个接口的入参和对 RE…

windows docker desktop 更换镜像 加速

最近 docker hub 访问不了; 经过研究 可以通过添加 代理镜像网址 添加代理服务器的方式 实现完美访问 1添加镜像网站 修改成国内镜像地址就能享受到飞一般的速度&#xff0c;但有一个问题&#xff0c;部分站点镜像不全或者镜像比较老&#xff0c;建议使用多个镜像站。 https…

人工智能与脑机接口:开启人机融合的新时代

人工智能与脑机接口&#xff1a;开启人机融合的新时代 随着人工智能&#xff08;AI&#xff09;技术的飞速发展&#xff0c;我们正与一个全新的时代相遇——人工智能与脑机接口相融合的时代。这个时代将带来前所未有的变革&#xff0c;让人类与机器的交互方式发生根本性的改变。…

React的useEvent 和 ahooks 的 useMemorizedFn 的深度分析和对比

父组件 const TestParent: React.FC<any> () > {const [State, setState] useState(0);const changeFun useCallback(() > {console.log(useCallback closure 里的 State, State);}, [State]);const changeFun_useEvent useEvent(() > {console.log(useEv…

新工业革命?基于机器视觉技术分拣机器人的未来与发展

原创 | 文 BFT机器人 01 分拣机器人的应用 基于机器视觉技术的分拣机器人可以将工人从繁重的劳动中解放出来&#xff0c;大大提高了分拣的效率&#xff0c;因此被广泛地应用于食品、物流以及煤矿等多个行业。 1.1 分拣机器人在水果分拣中的应用 随着农业科技的发展和人民生活…