小吉我今天更新了,惊不惊喜,意不意外,更新频率非常好(棒棒的)。小吉计划把二叉搜索树的知识更新完(预计在这几天更完),然后会有一段时间停更,因为小吉我要准备期末考试(几周时间速成🤪),玩归玩,闹归闹,绝对不能挂科!也祝大家期末考试门门过💪
言归正传,接下来就要进入我们今天的学习了,上一篇blog讲到put(存储键值和对应的实值)这个方法,这篇blog主要讲二叉搜索树的前任后任和删除操作,浅浅预告一下,这篇blog讲解的内容会有一些难🫣
实现大纲:
predecessor——查找键值的前任(即前驱节点)
successor——查找键值的后任(即后继节点)
deletenode——根据键值删除
predecessor——查找键值的前任(即前驱节点)并返回前驱节点的实值
实现之前先解释一下什么是前任(不是我们传统意义上理解的前任啦🫣),假设传参的键值是4,它的前任就是3,就是键值的前一个数
有可能有小伙伴第一反应是用中序遍历(左值右)求解,以上图为例,中序遍历的结果就是1234567,那前任就很好找了,但实际上我们并不用中序遍历的方式实现,因为性能不高(what a pity)
前任思路:1)节点有左孩子,此时的前驱节点就是左孩子中的最大值
2)节点没有左孩子,若离它最近的祖先自从左而来,此祖先即为前驱节点
在上面思路中,情况一比较好理解,相信很多小可爱看情况二就会比较难理解,没事,小吉我最开始也是一脸懵,接下来小吉画一张图来帮助大家解释一下
现在小伙伴们大致的思路应该明白了,可到了真正实现时,又有疑惑,我们应该怎么找到离它最近的从左而来的祖先呢?
我们传参的是一个键值,在找前驱节点之前,我们应该先遍历二叉搜索树,查找传入的键值是否存在,在这个过程中我们就可以创建一个指针,当往右时,把节点记录下来,最后指针的指向就是我们要找的离它最近且自左而来的节点
代码实现👇:
string BSTTree::predecessor(int key)
{
BSTnode* p = _root;
BSTnode* ancestorFromleft = nullptr;
while (p != nullptr)//先查找键值是否存在
{
if (p->_key > key)
{
p = p->_left;
}
else if (p->_key < key)
{
ancestorFromleft = p;
p = p->_right;
}
else
{
break;//找到退出循环
}
}
if (p == nullptr)
{
return "没找到节点";
}
//存在键值后找键值的前任
//找到节点 情况1:节点有左子树,此时前任就是左子树的最大值
if (p->_left != nullptr)
{
return max(p->_left);
}
//找到节点 情况2:节点没有左子树,若离它最近的、自左而来的祖先就是前任
return (ancestorFromleft != nullptr) ? ancestorFromleft->_value : "没有找到前任";
}
注:用到的max函数是我们上一篇blog中实现的方法,如果有小伙伴对此有问题,可以先去看小吉的上一篇blog,blog传送门🚪:c++实现二叉搜索树(上)
successor——查找键值的后任(即后继节点)并返回后继节点的实值
找后任和找前任非常相似,可以说是换汤不换药,所以小吉在这里只讲思路,不做过多的解释
思路:1)节点有右子树,此时后继节点即为右子树的最小值 2)节点没有右子树,离它最近的祖先自从右而来,此时祖先即为后继节点
代码实现:
string BSTTree::successor(int key)
{
BSTnode* p = _root;
BSTnode* ancestorFromright = nullptr;
while (p != nullptr)
{
if (p->_key > key)
{
ancestorFromright = p;
p = p->_left;
}
else if (p->_key < key)
{
p = p->_right;
}
else
{//找到就退出循环
break;
}
}
if (p == nullptr)
{
return "没有找到节点";
}
//找到节点 情况1:节点有右子树,此时后任就是右子树的最小值
if (p->_right != nullptr)
{
return doMin(p->_right);
}
//找到节点 情况2:节点没有右子树,离它最近,自右而来的祖先就是后任
return (ancestorFromright != nullptr) ? ancestorFromright->_value : "没有找到后任";
}
deletenode——根据键值删除
❗❗❗在讲实现之前,小吉先来强调点东西,二叉搜索树中树节点由new开辟,在删除节点时要用delete手动释放,要不然会导致内存泄漏,造成巨大的隐患(不仅在提醒大家,也在提醒小吉自己,因为小吉我经常忘记,这是一个非常不好的编码习惯!)
思路(分为四种情况):
1.删除节点没有左孩子,将右孩子托孤给要删除节点的父节点(parent)
2.删除节点没有右孩子,将左孩子托孤给要删除节点的父节点(parent)
3.删除节点的左右孩子都没有(其实已经被涵盖在情况1和情况2中),把nullptr托孤给要删除节点的父节点(parent)
4.删除节点左右孩子都有,可以将它的后继节点(简称s)托孤给要删除节点的父节点(parent),称s的父节点为sp
(在情况四的基础上又分为两种情况)
1)sp就是被删除节点,此时D(要删除的节点)与s紧邻,只需将s托孤给parent
2)sp不是被删除节点,此时D与s不紧邻,此时需要将s的后代托孤给sp,再将s托孤给parent
哈哈,相信很多小可爱看完上面的思路,跟没思路没什么差别,别急,接下来小吉结合图好好讲解一下
前三种情况已经分析完了,我们可以先写代码实现前三种情况,第四种情况比较复杂,我们等下细讲
前三种都涉及到托孤,我们先实现托孤这个方法
void BSTTree::shift(BSTnode* parent, BSTnode* deleted, BSTnode* child)
{//parent被删除节点的父亲 deleted被删除节点 child被顶上去的节点
if (parent == nullptr)
{
_root = child;
}
else if (deleted == parent->_left)
{
parent->_left = child;
}
else
{
parent->_right = child;
}
}
string BSTTree::deletenode(int key)
{
BSTnode* p = _root;
BSTnode* parent = nullptr;
while (p != nullptr)
{
if (p->_key > key)
{
parent = p;
p = p->_left;
}
else if (p->_key < key)
{
parent = p;
p = p->_right;
}
else
{
break;//找到就退出循环
}
}
if (p == nullptr)
{
return "没有找到要删除的节点";
}
//删除操作
if (p->_left == nullptr )
{//情况1:删除的节点没有左孩子,将右孩子托孤给parent
shift(parent, p, p->_right);
}
else if (p->_right == nullptr)
{//情况2:删除的节点没有右孩子,将左孩子托孤给parent
shift(parent, p, p->_left);
}
else
{
//情况四
}
string str = p->_value;
delete p;
p = nullptr;//置空,防止悬挂指针
return str;
}
注:情况三走情况一或情况二的代码都能实现
🤪🤪🤪情况四讲解
情况四代码实现总结:
1)被删除节点找后继
2)处理后继节点的后事
3)后继节点取代被删除节点
代码实现:
else
{//情况4:1.被删除节点找后继 2.处理后继的后事 3.后继取代被删除节点
BSTnode* s = p->_right;
BSTnode* sparent = p;
while (s->_left != nullptr)
{
sparent = s;
s = s->_left;
}
if (sparent != p)
{
shift(sparent, s, s->_right);//s(后继)不可能有左孩子
s->_right = p->_right;
}
shift(parent, p, s);
s->_left = p->_left;
}
👆以上四种情况已经实现完了,二叉搜索树删除操作已实现,上面是使用非递归的方式实现,下面小吉将呈现用递归的方式实现删除操作,大致思路和非递归相同,小吉就不做过多的解释了,直接上代码
//返回值为删除节点后剩下的孩子节点 参数node是删除的起点
BSTnode* BSTTree::doDelete(BSTnode* node, int key,vector<string>& v)//!!!传引用
{
if (node == nullptr)
{//如果没找到节点,返回nullptr
return nullptr;
}
if (node->_key < key)
{
node->_right=doDelete(node->_right, key,v);
return node;
}
if (node->_key > key)
{
node->_left=doDelete(node->_left, key,v);
return node;
}
//找到节点
v.push_back(node->_value);
//1.只有右孩子
if (node->_left == nullptr)
{
BSTnode* temp = node;
node = node->_right;
delete temp;
temp = nullptr;
return node;
}
//2.只有左孩子
if (node->_right == nullptr)
{
BSTnode* temp = node;
node = node->_left;
delete temp;
temp = nullptr;
return node;
}
//3.有两个孩子(后继节点)
BSTnode* s = node->_right;
while (s->_left != nullptr)
{
s = s->_left;
}
vector<string> v1;
s->_right=doDelete(node->_right, s->_key,v1);
s->_left = node->_left;
BSTnode* temp = node;
delete temp;
temp = nullptr;
return s;
}
string BSTTree::deletenode2(int key)
{
vector<string> v;
_root = doDelete(_root, key, v);
string str = v[0];
if (v.empty())
{
return "没有找到节点";
}
return str;
}
❗这里强调一下实现递归的doDelete函数,只要牢记返回的是删除节点后剩下的孩子节点和传入的是删除的起点,这样递归函数就很好理解了
这篇blog有点难理解,尤其是根据键值删除节点的部分,大家没看懂不要急,多看几遍,多画画图,多写写代码就好了,不要否定自己,你其实很棒的,慢慢来,多给自己一点时间,多点耐心❤️
学习的时间总是短暂的,这篇blog到这里就已经全部结束了(完结撒花🎉),小吉写这篇blog差不多花了三个小时(着实有点小累),希望大家多多点赞收藏关注(爱你们哦😘❤️😘)
如有错,还望各位大佬多多指点🌹🌹🌹