当前位置: 首页 > news >正文

C++学习记录(13)二叉排序树

前言

简单回顾一下二叉树。

硬说的话我们其实学习的只有堆和普通的二叉树。

其中堆的逻辑结构是完全二叉树,物理结构底层其实是顺序存储,一般直接就用数组代替了;

普通二叉树我们学习的重点其实就是大概了解名词、了解结构、前中后序、层序遍历等。

其实大部分都是以二叉树的结构为主,但是实际上搞个普通二叉树有什么用呢?

就拿最简单的查找来说,同样的数据vector、list都是一个一个往后找,问题它们遍历方式多简单,大部分情况直接++就行,你这个普通二叉树一会又得左一会又得右,还得在这个基础上防止空指针的解引用,如果遍历到树的底还得返回,而且你咋知道啥时候才能便利完,这也是个麻烦事,用起来可以说是太臭了,根本不讨喜,实际应用中也确实没人使用。

一、二叉排序树(二叉搜索树)

二叉排序树(二叉搜索树),可以是一棵空树,也可以是一棵符合以下条件的树:

  • 若二叉排序树的左子树不为空,则它左子树的所有结点的值小于根节点
  • 若二叉排序树的右子树不为空,则它右子树的所有结点的值大于根节点
  • 二叉排序树的左右子树也分别为二叉排序树

二叉排序树中可以插入相等的值也可以插入不相等的值,这里讲二叉排序树就是为后续的容器map/set/multimap/multiset做准备,它们的底层就是二叉排序树,而map/set不允许插入相同的数据,multimap/multiset允许插入相同的数据。

并且二叉排序树非常厉害的一点就是,中序遍历的序列是一个递增序列:

第一个图:1 3 4 6 7 8 10 13 14

第二个图:1 3 3 6 7 8 10 10 13

二、二叉排序树性能分析

由于左子树的数值一定小于根结点,右子树的数值一定大于根结点且所有子树都符合,所以查找一个数的时候最坏的时间复杂度大概也就是:O(logN)那是因为我们分析性能时看的是:

但是如果是这样的一棵树呢?

让你查找1你不炸了,所以二叉排序树还是有缺陷的,毕竟这种类似于单支树可以说完全当成链表用了都,时间复杂度就是O(N)。

分析时间复杂度肯定是看的最差的那二叉排序树的时间复杂度就是O(N)。

因此普通二叉树升级成二叉排序树其实还是不够好,后续我们会再学二叉排序树的升级版本,不过现在还是先吃透二叉排序树吧,小心步子太大,扯。

三、二叉排序树的插入

大概框架整出来,类比list,先整个Node:

template <class K>
struct BSTNode
{BSTNode(const K& x):_key(x), _left(nullptr), _right(nullptr){}K _key;struct BSTNode* _left;struct BSTNode* _right;
};template <class K>
class BST
{typedef BSTNode<K> Node;private:Node* _root = nullptr;
};

大概就是这样的,唯一特殊的可能就是在这里二叉排序树的场景下,更喜欢把二叉树里面存的值叫做关键字key,其它的像什么左孩子指针右孩子指针、typedef都没什么好说的,就那些玩意。

正式进入插入操作昂:

随便拉过来个图,其实多的不用说啥,插入的值必须满足二叉排序树的规则,左子树比根小、右子树比根大,写起来代码大概就是:

if(key < _key)

        root = root->left;

else if(key > _key)

        root = root->right;

else

        //相等两边都能挂

直到找到空指针,找到空指针以后等于就得插,直接插就行。当然,我们这里实现二叉排序树的操作,就不管相等的事了,就认为我们的二叉排序树不允许相等的存在,如果遇到key = _key,那就直接不插了。

随便举个例子:

搞个key = 5这样插入的时候判断的多一点,更能覆盖到所有情况,大眼一看其实大概知道结果是这样的:

为了更好的了解代码思路,我们一步一步走:

有了图以后写代码就好说了。

1.第一个要点

我上来就猛猛干:

	bool Insert(const K& x){while (_root){if (x < _root->_key)_root = _root->_left;else if (x > _root->_key)_root = _root->_right;elsereturn false;}}

写着写着我发现不对劲了,我如果直接用root去遍历,那岂不是把二叉树的根直接改了,所以还得搞个临时变量遍历:

	bool Insert(const K& x){Node* cur = _root;while (cur){if (x < cur->_key)cur = cur->_left;else if (x > cur->_key)cur = cur->_right;elsereturn false;}}

while循环找到了x应该插入的地方了,也就是:

2.第二个要点

new个结点再插入到二叉树就行了,但是我们cur现在的值是4->right = nullptr,并没有记录4的结点啊,没法插,所以还得费劲搞个变量记录一下这个需要被插的叶子结点:

	bool Insert(const K& x){Node* cur = _root;Node* parent = _root;while (cur){if (x < cur->_key){parent = cur;cur = cur->_left;}else if (x > cur->_key){parent = cur;cur = cur->_right;}elsereturn false;}cur = new Node(x);if (x < parent->_key)parent->_left = cur;else if (x > parent->_key)parent->_right = cur;return true;}

3.第三个要点

然后又检查,仔细一看又忘了,二叉排序树完全可以是空树,如果是空树while循环直接跳过,那就直接匹配parent ->_key,碰见空指针真就坏事了,再加个拦截:

	bool Insert(const K& x){if (_root == nullptr){_root = new Node(x);return true;}Node* cur = _root;Node* parent = _root;while (cur){if (x < cur->_key){parent = cur;cur = cur->_left;}else if (x > cur->_key){parent = cur;cur = cur->_right;}elsereturn false;}cur = new Node(x);if (x < parent->_key)parent->_left = cur;else if (x > parent->_key)parent->_right = cur;return true;}

4.要点四和五

上面的代码再干看着下去我也不知道对不对,但是再补充两点:

其实我也不知道为啥二叉排序树的返回值都弄成bool值,但是我学习的时候看大佬都是这么弄的,我实在好奇问了问ai:

二叉排序树的操作经常返回 bool值,主要是为了​​清晰报告操作的成功与否​​,核心目的是​​维护二叉排序树的关键特性(键值唯一),并保持接口简洁​​。这种设计在多数情况下是直观有效的。当需要更多信息时,可以考虑返回指针、迭代器等设计。

而这里的true表示二叉排序树中没有相同值,成功插入了;

false表示二叉排序树中有相同值,因此没有成功插入。


再来就是我设计成非递归而非递归大致理由是这样的,如果vector、list遍历最差也就是个O(N),问题他们没有这个函数栈帧的疯狂开辟,不用担心栈溢出的问题。

假如你把二叉排序树的插入操作搞成个递归实现,那么碰见:

插入0试试呢?开基本上N个函数栈帧,如果数据量大的话是不是直接把栈帧弄干了。

而且递归设计一般是啥吧,非递归写起来跟石一样才搞个递归降低代码编写的难度,其实我代码已经干出来了,不管对不对吧,大致思路就是这样,其实也没说难的看不懂。

5.中序遍历

为了方便测试,直接写个中序遍历吧,不然干看插入的每一句代码或者看监视窗口能给我看似。

	void InOrder(Node* root){if (root == nullptr)return;InOrder(root->_left);cout << root->_key << " ";InOrder(root->_right);}

这个写个递归真不犯毛病,如果有N个数据,最多也就开logN个函数栈帧,其实老省事了。

当然,其实还是有问题,按道理来说我根据二叉排序树的实例化对象直接调用中序遍历函数逻辑都是确定的啊,咋滴还得我传二叉树的根节点,这函数明显就是这意思嘛,而且根节点private了,你去哪给我拿根,再说了真给你你套到里面,你会发现根本就是无穷递归,因为每次都是用_root去访问_left、_right。

所以:

	void InOrder(){_InOrder(_root);}private:void _InOrder(Node* root){if (root == nullptr)return;_InOrder(root->_left);cout << root->_key << " ";_InOrder(root->_right);}

包装一下。

没毛病噢,反正中序跟插入没啥事了,因为中序真不能有啥问题,逻辑就那么简单,插入没错中序才能对。

四、二叉排序树的查找

逻辑差不多,因为其实插入差不多就是先找到对应的位置才插了,那查找就也没那么费劲:

	bool Find(const K& x){Node* cur = _root;while (cur){if (x < cur->_key)cur = cur->_left;else if (x > cur->_key)cur = cur->_right;elsereturn true;}return false;}

只不过找到相等的就返回true;

遍历到空指针了都,说明压根没有对应的值,返回false。

没啥毛病噢。

当然,多嘴一下,因为我们Find函数的内涵是这样的,存在相同元素时要求查找到的应该是中序遍历序列里相同元素的第一个元素:

但是根据这个图走读代码发现其实不对劲,中序遍历第一个应该是碰到下面那个3才true。

当然后续再说好吧,因为现在我们只关心有没有,查找到中序遍历的第一个是为了其它实现,在这不多说。

五、二叉排序树的删除

1.思路细解

二叉排序树的删除却成个麻烦事了,因为啥吧:

删除无左孩子右孩子的结点(叶子结点);

删除有左孩子无右孩子的结点;

删除有右孩子无左孩子的结点;

删除既有左孩子又有右孩子的结点。

重点你删也能硬说的过去,但是删完以后二叉排序树还得是二叉排序树啊,不能说你拍拍屁股一删了之,留了一地鸡毛,那你不是找事呢嘛。

所以根据图一点一点分析。

无论删除哪一个结点,我们要做的都是先知道那个结点在哪,并且找到以后删除时要注意维护好二叉排序树的性质。

删除1,即删除叶子结点

删除叶子结点其实很简单,唯一需要注意的点就是你直接delete有问题:

直接delete,那它的父结点的left/right直接干成指向已经释放空间的地址,也就是野指针了。所以由最简单的情况我们得知,不仅仅要用cur找到需要删除的结点,还需要额外搞一个parent,到时候把该置空的指针置空。

删除10,即删除只有右孩子的结点

删除14,即删除只有左孩子的结点

其实也没那么麻烦:

可以观察到如果需要删除的结点只有右子树的话,到时候直接把cur的子树上提代替cur的位置即可。

甚至可以推广一下:

cur如果只有右子树,不管cur在parent的左还是右,都直接上提就行。

比如看这里的cur = 10,parent = 8,cur所在的如果是parent的右子树,删去cur以后剩余结点一定也比parent大,直接上提在不影响原树二叉排序树性质的情况下,也保证parent右子树符合二叉排序树。

再来可以看cur = 3,parent = 8,cur所在的是parent的左子树,删去cur以后剩余结点一定小于parent,直接上提以后不影响原树符合二叉排序树性质,还能保证parent左子树符合二叉排序树性质。

甚至14都不用看了,在此基础上继续推广:

想象一下现在cur = 3和cur = 10都是只有左子树,是不是它们的左子树也都小于或者大于parent,直接上提不影响原树二叉排序树性质,还能维护好parent的二叉排序树性质。

删除8,即删除既有左子树又有右子树的结点

这事整的真是麻烦,因为啥吧,原来只有一棵子树或者没有子树的时候基本上都是一带而过,修改修改parent的left/right结点再delete就完事了。如果有两棵子树,直接delete那么cur指向的结点的左右两棵子树直接失去联系了,删一个结点少一大堆结点,那不坏事了嘛。

这里直接给出标准答案:

既然直接删去根节点是非常危险的事,那我们不妨将它转换为叶子结点,即与cur的左子树的最大值交换或者与cur的右子树的最小值交换。

左子树的最大值肯定是大于左子树所有值,它当根节点首先保证了它是大于左子树所有值或者说左子树所有值小于它;站在原树的角度,它就算是原树左子树最大值,那也是小于根的,也就是小于右子树的所有值,因为右子树的设定就是大于根节点的值。

即原树左子树除其外所有值<原树左子树最大值<根结点值<原树右子树所有值

当根很好的维护了二叉排序树的性质。

同理可以得到原树左子树所有值<根结点值<原树右子树的最小值<原树除其外所有值。

有了这个性质我们知道怎么弄了,但是去哪找到左子树的最大值或者右子树的最小值呢?

这就得随便研究一个二叉排序树了:

一直往左走直到走不动是不是就是中序遍历的第一个也是整个二叉排序树最小值;

一直往右走直到走不动是不是就是中序遍历最后一个也是整个二叉排序树最大值。

所以找被删结点有左右子树找左子树最大就从cur左子树一直往右;找右子树最小就从cur右子树一直向左。

另外就是假如我们要找左最大:

交换以后delete8就行了,但是6的右孩子没删,所以还得在寻找左最大(右最小)时搞个parent。

2.代码表达

	bool Erase(const K& key){if (_root == nullptr)return false;Node* cur = _root;Node* parent = _root;while (cur){if (key < cur->_key){parent = cur;cur = cur->_left;}else if (key > cur->_key){parent = cur;cur = cur->_right;}else//找到相等的点,准备删除逻辑{//删除的是叶子结点if (cur->_left == nullptr && cur->_right == nullptr){if (key < parent->_key)parent->_left = nullptr;elseparent->_right = nullptr;delete cur;cur = nullptr;return true;}//删除的是只有左/右子树的结点else if (cur->_left == nullptr || cur->_right == nullptr){if (cur->_left != nullptr)if (key < parent->_key)parent->_left = cur->_left;elseparent->_right = cur->_left;elseif (key < parent->_key)parent->_left = cur->_right;elseparent->_right = cur->_right;				delete cur;cur = nullptr;return true;}//删除的是左右子树都有的结点else{//左子树最大在最右边Node* leftmax = cur->_left;Node* leftmaxparent = cur->_left;while (leftmax->_right != nullptr){leftmaxparent = leftmax;leftmax = leftmax->_right;}swap(leftmax->_key, cur->_key);leftmaxparent->_right = nullptr;delete leftmax;////右子树最小在最左边//Node* rightmin = cur->_right;//Node* rightminparent = cur->_right;//while (rightmin->_left != nullptr)//{//	rightminparent = rightmin;//	rightmin = rightmin->_left;//}//swap(rightmin->_key, cur->_key);//rightminparent->_left = nullptr;//delete rightmin;leftmax = nullptr;return true;}					}}//压根没找到key值return false;}

最外层while就是去找到底有没有key这个结点,如果有根据是叶子结点、只有一个子树的结点、有两个子树的结点分别写代码处理delete。

然后我也没调试,我读代码的时候,发现这个条件有点疑问:

				//删除的是左右子树都有的结点else{//左子树最大在最右边Node* leftmax = cur->_left;Node* leftmaxparent = cur->_left;while (leftmax->_right != nullptr){leftmaxparent = leftmax;leftmax = leftmax->_right;}swap(leftmax->_key, cur->_key);leftmaxparent->_right = nullptr;delete leftmax;////右子树最小在最左边//Node* rightmin = cur->_right;//Node* rightminparent = cur->_right;//while (rightmin->_left != nullptr)//{//	rightminparent = rightmin;//	rightmin = rightmin->_left;//}//swap(rightmin->_key, cur->_key);//rightminparent->_left = nullptr;//delete rightmin;leftmax = nullptrreturn true;}					

主要是针对其内部查找左子树最大右子树最小的时候parent的问题,比如我们有图:

这么一看,如果左右子树只有一个结点的话我们写的代码cur指向的不就是野指针,如果来个中序遍历那不就是野指针的解引用。

因为我们这段代码写的时候其实是这么看的:

写代码的是时候认为不止一个结点,但是极端情况很可能存在子树最值路径就只有一个结点,比如这种情况:

初始化的时候rightminparent就是cur,这样初始化就符合它是rightmin的parent,如果只有一个结点,循环没进去,还swap了,此时rightminparent == cur,那这个时候应该rightminparent->_right = nullptr,否则就是正常的进入子树了,那么rightminparent的左一定是rightmin。

即如果删除的是左右子树都有的结点时还得注意最值路径仅有一个结点的问题。

				//删除的是左右子树都有的结点else{//左子树最大在最右边Node* leftmax = cur->_left;Node* leftmaxparent = cur;while (leftmax->_right != nullptr){leftmaxparent = leftmax;leftmax = leftmax->_right;}swap(leftmax->_key, cur->_key);if (leftmaxparent == cur)//左子树最大结点路径只有一个元素leftmaxparent->_left = nullptr;elseleftmaxparent->_right = nullptr;delete leftmax;leftmax = nullptr;////右子树最小在最左边//Node* rightmin = cur->_right;//Node* rightminparent = cur->_right;//while (rightmin->_left != nullptr)//{//	rightminparent = rightmin;//	rightmin = rightmin->_left;//}//swap(rightmin->_key, cur->_key);//if (rightminxparent == cur)//右子树最小结点路径只有一个元素//	rightminparent->_right = nullptr;//else//	rightminparent->_left = nullptr;//delete rightmin;//rightmin = nullptr;return true;}					

测试了测试没啥毛病,把剩下的都清了试试:

结果就又炸了,这个确实我就没有走读代码走读出来,但是大致就是删7的时候出的事,毕竟删完6打印没啥毛病,那就调试:

不难看出此时二叉排序树的结构为:

把代码拉过来走读代码看看犯啥毛病了:

删除的就是根节点,根节点符合只有一个右子树的结点,此时cur指向7,parent指向7,按照正常处理删除的结点只有一棵子树的时候cur和parent的相对位置是这样的:

这也倒是提醒我了,初始parent的值搞成nullptr更好一点,因为初始cur = _root,营造一个类似于悬空父结点的感觉。

至于这个问题那就转变为空指针的解引用问题(其实空指针报错都比不声不响炸了好,毕竟至少测试看出来赶紧改,初始那种情况跑半天最后跑出来个程序不正常运行)。

干脆拦截一下:

				//删除的是只有左/右子树的结点else if (cur->_left == nullptr || cur->_right == nullptr){if (cur->_left != nullptr){//如果碰到parent悬空即只有两个结点if (parent == nullptr)_root = cur->_left;elseif (key < parent->_key)parent->_left = cur->_left;elseparent->_right = cur->_left;}else{if (parent == nullptr)_root = cur->_right;elseif (key < parent->_key)parent->_left = cur->_right;elseparent->_right = cur->_right;}delete cur;cur = nullptr;return true;}

其实吧,学指针的时候都想的是,指针解引用前一定不能为空,但是一旦专注于主体逻辑,这种细节处理就没管,有点小可悲,长点心吧。

这一个parent督促着我去检查了检查叶子结点和两个子树结点的删除,其中叶子结点删除也存着这样的问题,如果二叉排序树只有一个结点就是空指针解引用,至于两个子树的删除根本用不到parent。

	bool Erase(const K& key){if (_root == nullptr)return false;Node* cur = _root;Node* parent = nullptr;while (cur){if (key < cur->_key){parent = cur;cur = cur->_left;}else if (key > cur->_key){parent = cur;cur = cur->_right;}else//找到相等的点,准备删除逻辑{//删除的是叶子结点if (cur->_left == nullptr && cur->_right == nullptr){//二叉排序树只有一个结点if (parent == nullptr){delete cur;_root = nullptr;return true;}if (key < parent->_key)parent->_left = nullptr;elseparent->_right = nullptr;delete cur;cur = nullptr;return true;}//删除的是只有左/右子树的结点else if (cur->_left == nullptr || cur->_right == nullptr){if (cur->_left != nullptr){//如果碰到parent悬空即只有两个结点if (parent == nullptr)_root = cur->_left;elseif (key < parent->_key)parent->_left = cur->_left;elseparent->_right = cur->_left;}else{if (parent == nullptr)_root = cur->_right;elseif (key < parent->_key)parent->_left = cur->_right;elseparent->_right = cur->_right;}delete cur;cur = nullptr;return true;}//删除的是左右子树都有的结点else{//左子树最大在最右边Node* leftmax = cur->_left;Node* leftmaxparent = cur;while (leftmax->_right != nullptr){leftmaxparent = leftmax;leftmax = leftmax->_right;}swap(leftmax->_key, cur->_key);if (leftmaxparent == cur)//左子树最大结点路径只有一个元素leftmaxparent->_left = nullptr;elseleftmaxparent->_right = nullptr;delete leftmax;leftmax = nullptr;////右子树最小在最左边//Node* rightmin = cur->_right;//Node* rightminparent = cur->_right;//while (rightmin->_left != nullptr)//{//	rightminparent = rightmin;//	rightmin = rightmin->_left;//}//swap(rightmin->_key, cur->_key);//if (rightminxparent == cur)//右子树最小结点路径只有一个元素//	rightminparent->_right = nullptr;//else//	rightminparent->_left = nullptr;//delete rightmin;//rightmin = nullptr;return true;}					}}//压根没找到key值return false;}

需要注意的是悬空问题,说明删除的是根节点,如果删除的是根节点,那就必须更新根节点即_root的值,因此:

因为其它情况下都是通过结点修改_left和_right,这里直接动_root了。

应该没啥毛病了:

盖了帽了真是,其实情况就四个,但是细节处理太多了,导致if-else套太多层了最后测试完给我看笑了,像屎山。

六、二叉搜索树key和key/value场景

上面整了这么多,反正纯纯当成二叉排序树用了,因为测试都是用中序遍历搞的。

但是实际上搜索也是非常强力的,当然,暂时不考虑那种二叉树退化成链表的情况,因为后续我们还会学习AVL树和红黑树,到时候有平衡因子的事,不会说让二叉树“甩尾”。

1.key的使用场景

  • 门禁:系统存储所有授权车辆的车牌号(key)。车辆进入时,扫描车牌并查询其是否在二叉搜索树中,存在则放行
  • 将字典中的所有单词构建成Key模型的BST。检查文章单词时,快速查询该单词是否在词典中,不在则提示拼写错误

有实际例子了我们能更切身实际的考虑二叉搜索树的效率,上面说了噢,有平衡的事在,绝对标标准准的树,不是链表那样的树。

所以基本上最坏的搜索次数就是logN次,假如有一百万个车牌号,2^{10} = 1024,算数量级等于一百万个数据树大概就二十层,二十层遍历就拿我们写的Find,就算成循环的有10行代码,10*20,计算机执行200句代码,还不是跟玩一样,最多有个认证成功抬杆,认证失败不抬杆,可以说几毫秒就成了。

2.key/value使用场景

  • 字典翻译:构建一个BST,key是英文单词,value是对应的中文释义。输入英文单词即可快速查到其中文意思
  • 统计频数:例如统计水果出现次数。key是水果名称,value是出现次数。遍历数据,若水果不存在则插入(key, 1);若存在则将其对应value加1
  • 自助停车场计费:入场时记录车牌号(key)和入场时间(value)。出场时根据车牌号查找入场时间,计算停车费用

其实还是根据key建立二叉搜索树,只不过key伴随着一个value,这里key只是方便查找的关键字,value才是真正有用的。

比如就拿自助停车场收费系统来说,车牌号入二叉搜索树的时候带着入场时间value,等出停车场的时候,再根据二叉搜索树的特性快速找到value,(当前时间-value)*单位时间收费,很快就能算出来停车费是多少。

3.key/value树实现

其实这个没啥含金量,直接copy,该加value的地方加value,毕竟二叉搜索树的构建还是依据key,那只需要我们该加value的时候加一下就行:

namespace key_value
{template <class K,class V>struct BSTNode{BSTNode(const K& key, const V& value):_key(key), _value(value), _left(nullptr), _right(nullptr){}K _key;V _value;struct BSTNode* _left;struct BSTNode* _right;};template <class K,class V>class BST{typedef BSTNode<K,V> Node;public:bool Insert(const K& key,const V& value){if (_root == nullptr){_root = new Node(key,value);return true;}Node* cur = _root;Node* parent = nullptr;while (cur){if (key < cur->_key){parent = cur;cur = cur->_left;}else if (key > cur->_key){parent = cur;cur = cur->_right;}elsereturn false;}cur = new Node(key,value);if (key < parent->_key)parent->_left = cur;elseparent->_right = cur;return true;}Node* Find(const K& x){if (_root == nullptr)return nullptr;Node* cur = _root;while (cur){if (x < cur->_key)cur = cur->_left;else if (x > cur->_key)cur = cur->_right;elsereturn cur;}return nullptr;}bool Erase(const K& key){if (_root == nullptr)return false;Node* cur = _root;Node* parent = nullptr;while (cur){if (key < cur->_key){parent = cur;cur = cur->_left;}else if (key > cur->_key){parent = cur;cur = cur->_right;}else//找到相等的点,准备删除逻辑{//删除的是叶子结点if (cur->_left == nullptr && cur->_right == nullptr){//二叉排序树只有一个结点if (parent == nullptr){delete cur;_root = nullptr;return true;}if (key < parent->_key)parent->_left = nullptr;elseparent->_right = nullptr;delete cur;cur = nullptr;return true;}//删除的是只有左/右子树的结点else if (cur->_left == nullptr || cur->_right == nullptr){if (cur->_left != nullptr){//如果碰到parent悬空即只有两个结点if (parent == nullptr)_root = cur->_left;elseif (key < parent->_key)parent->_left = cur->_left;elseparent->_right = cur->_left;}else{if (parent == nullptr)_root = cur->_right;elseif (key < parent->_key)parent->_left = cur->_right;elseparent->_right = cur->_right;}delete cur;cur = nullptr;return true;}//删除的是左右子树都有的结点else{//左子树最大在最右边Node* leftmax = cur->_left;Node* leftmaxparent = cur;while (leftmax->_right != nullptr){leftmaxparent = leftmax;leftmax = leftmax->_right;}swap(leftmax->_key, cur->_key);if (leftmaxparent == cur)//左子树最大结点路径只有一个元素leftmaxparent->_left = nullptr;elseleftmaxparent->_right = nullptr;delete leftmax;leftmax = nullptr;////右子树最小在最左边//Node* rightmin = cur->_right;//Node* rightminparent = cur->_right;//while (rightmin->_left != nullptr)//{//	rightminparent = rightmin;//	rightmin = rightmin->_left;//}//swap(rightmin->_key, cur->_key);//if (rightminxparent == cur)//右子树最小结点路径只有一个元素//	rightminparent->_right = nullptr;//else//	rightminparent->_left = nullptr;//delete rightmin;//rightmin = nullptr;return true;}}}//压根没找到key值return false;}void InOrder(){_InOrder(_root);cout << endl;}private:void _InOrder(Node* root){if (root == nullptr)return;_InOrder(root->_left);cout << root->_key << ":" << root->_value << " ";_InOrder(root->_right);}private:Node* _root = nullptr;};
}
  • 结点类每个key都对应着一个value,所以多加一个类型和变量控制
  • Insert方法,构建二叉树的时候关键还是看key,只不过new的时候把value带上
  • Find方法,想想自助停车场的例子,其实还是看key,只不过根据key取出来相应的value,所以改了一下返回值,如果通过查找key能拿到对应结点的指针的话,简简单单就能取到value
  • Erase方法根本不用管,道理很简单,构建二叉树的时候需要根据key构建,value只不过是顺带的,等删节点的时候我们都是delete cur,也就是直接删除对应结点,所经历的就是析构+operator delete(),operator delete()的时候可不会心软,直接把所有变量就删了,因此逻辑根本不用变

测试计数场景

string arr[] = { "苹果", "西瓜","苹果", "西瓜", "苹果", "苹果","苹果","苹果", "西瓜", "苹果", "香蕉", "苹果", "香蕉" };key_value::BST<string, int> t;//第一次找到就入树//第二次找到就++valuefor (auto& str : arr){auto ret = t.Find(str);if (!ret)t.Insert(str, 1);elseret->_value++;}t.InOrder();

这个树这一点强到了,之前我们计数只能用变量去计数,而且我哪知道能有多少种情况,但是有二叉排序树以后,既存key又存value,不用再担心有多少个key必须创建多少个value的问题。

测试字典场景

	key_value::BST<string, string> dict;dict.Insert("propose", "求婚;提议,建议");dict.Insert("expose", "暴露,揭露");dict.Insert("impose", "征税;强加,强迫");dict.Insert("oppose", "否认,反对");string str;while (cin >> str){auto ret = dict.Find(str);if (ret)cout << ret->_key << "->" << ret->_value << endl;elsecout << "单词不存在,请重新输入!" << endl;}

铺垫基本结束,马上开始map和set的学习。

http://www.dtcms.com/a/411396.html

相关文章:

  • TongWeb下如何获取数据源的物理连接?
  • 保险资料网站有哪些三网合一网站建设报价
  • 网站建设系统分析ai的优点和缺点
  • 三网合一网站百度一下免费下载
  • 坤驰科技携数据采集解决方案,亮相中国光纤传感大会
  • 可以做免费的网站吗广州平面设计工作室
  • 【文献阅读】基于机器学习的网络最差鲁棒性可扩展快速评估框架
  • 【复习】计网每日一题--PPP协议透明传输
  • 【训练技巧】torch.amp.GradScaler 里面当scale系数为0或者非常小的时候,详细分析与解决思路
  • 一站式服务logo设计深圳网站建设服务商哪些好?
  • 专业的网站建设公司电话做商城网站要什么手续
  • mdBook 开源笔记
  • 【1、Kotlin 基础语法】2、Kotlin 变量
  • TorchV知识库安全解决方案:基于智能环境感知的动态权限控制
  • 网站后台演示2023小规模企业所得税税率是多少
  • 常见设计模式讲解
  • 怎么查网站备案服务商房地产新闻动态
  • php做网站主题建设项目一次公示网站
  • 同城外卖系统技术解析:SpringBoot如何赋能区域外卖突围战
  • .NET Framework 4.0.30319:官方下载与常见问题解决指南
  • 池州网站优化有没有网站做字体变形
  • 【论文阅读 | ICCV 2025 | M-SpecGene:面向 RGBT 多光谱视觉的通用基础模型​​】
  • 江苏省省建设厅网站公司的介绍怎么写
  • 专门做二手手机的网站吗网站建设 协议书 doc
  • Kubernetes Headless Service 深度解析 —— 用大白话讲清楚
  • 做网站的软件pageseo策略
  • 怀化冰山涯IT网站建设公司电子商务网站开发背景和意义
  • 免费设立网站企业对比网站
  • j2ee 建设简单网站数据分析师要学什么
  • LeetCode 1023.驼峰式匹配