平衡树(Splay) 服务:第二弹——插入,删除,查询
0.前言
今天有奥赛课,所以我又回来了
今天会把普通平衡树的操作讲完
1.插入
首先,在splay中,是不会有权值(也就是平衡树排大小的关键字)重复的结点的。取而代之的,是表示这个值出现次数的附加值cnt.
插入操作可以分为几种情况:
1.平衡树中什么也没有,这个时候我们只要新建第一个结点,并把它设为根节点就可以啦。
2.平衡树中已经出现过了这个权值。我们找到这个权值所在的位置,把这个结点的size值和cnt值都加一即可
3.这个权值在平衡树中还没有出现过。我们找到这个权值所应该在的位置,并在这个位置上新建一个结点,并把size值和cnt值都初始化为1
2,3的最后,都要把找到或新建的结点旋转到根节点
如何找出要插入到权值所应该在的位置?
我们从根结点往下找,判断要插入的权值和当前结点权值的大小。如果大于当前结点,就去它的右子树,否则就去它的左子树,直到两个权值相等或当前结点不存在
建立新结点的函数代码
void nowPoint(int val, int fa, int nxt) //新建一个权值为val,是fa结点的nxt孩子的结点
{
tot++;
fa(tot) = fa, size(tot) = cnt(tot) = 1, val(tot) = val;
son(fa, nxt) = tot;
}
插入函数的代码
void insert(int val) //插入一个值为val的数
{
if (root == 0)
{
nowPoint(val, 0, 1);
root = tot;
return;
}
int now = root;//查找部分在下面查找部分有比较详细的注释来解释
while (1)
{
size(now)++;//让所有子树包含val结点的结点size值加1
if (val(now) == val)
{
cnt(now)++, size(now)++;
splay(now,root);
return;
}
int nxt = val(now) < val, son = son(now, nxt);
if (!son)
{
nowPoint(val, now, nxt);
splay(tot, root);
return;
}
now = son;
}
}
2.删除
接下来就是splay最恶心最容易出锅的地方了(至少对我来说是这样)
什么是删除?
我以前听过一句话:一个人一生会经历三次死亡。第一次是生理上的死亡,第二次是最后一个认识你的人的死亡,第三次是所有能证明你存在的事物的消失。
在删除操作上也是同理:如何证明一个点被删除了?我们只要让它不是其他任何一个点的父节点,左孩子和右孩子就可以了。
在删除之前,我们首先要实现一个函数:find()。它的功能是查找一个权值为val的结点的编号,并把它旋转到根节点。如果不存在,就返回0
插入中有类似的代码,我就不再过多解释了
查询函数的代码
int find(int val)//查找val值对应的编号并旋转到根节点
{
int now = root//从根结点向下查找
while (1)
{
if (!now)//结构体中的值默认为0.如果now值为0,说明查找到了一个不存在的结点,也就是说这个权值在以前并没有被插入过,因此我们可以直接返回0
return 0;
if (val(now) == val)//如果有我们查到了这个权值
{
splay(now, root);//就把这个权值对应的结点旋转到根节点
return now;//返回这个结点对应的编号
}
int nxt = val(now) < val, son = son(now, nxt);//如果val(now)<val,val对应的结点一定在右子树中,而此时val<val(now)的返回值恰好为1,也正表示右孩子
now = son;
}
}
我们进行删除操作的第一步,就是find一下val的编号。如果需要删除一个不存在的值,那么直接退出。
在find中,我们已经把val旋转到了根节点。现在,比val小的数都在根节点的左子树,比find大的数都在根节点的右子树。
我们可以把删除操作分成以下几种情况。
1.val结点出现过多次,我们只把它的cnt值和size值-1就可以了
2.val既没有左子树,也没有右子树,此时整棵树中只有val一个数,此时我们直接把root赋为0,也就是整颗树还没有结点的初始状态
3.val有左子树,但没有右子树。我们直接把val的左结点的父亲暴力改成0结点,把val的左结点暴力改成根结点就可以了。现在,val不是根节点,无法被直接选择,也无法由其他任何一个结点所遍历到,那么它就相当于被删除了。就像这样
图中原来的1号结点实质上已经被删除了( 但是编号并没有被回收 )
4.val有右子树,但没有左子树,此时同2
5.val既有左子树,又有右子树
我们此时找出val的左子树中最大的一个点pos,把它移动到根节点。
那么此时pos的左子树为之前val除pos以外的左子树(pos为之前val左子树中的最大值,因此在旋转的过程中它不会有左孩子,只有在倒数第二部它旋转到了val的左孩子,再次旋转时val就会变成pos的右孩子),右子树为val和它以前的右子树(pos是小于val的最大的结点,显然没有一个结点的权值在大于pos而小于val,因此val不可能有左孩子)。
我们在暴力把val的右孩子和pos连在一起,val就相当于被孤立了。我们放图来演示一下这个过程:
开始时
pos旋转到了val的左孩子
pos旋转到了根节点
val被删除了
删除之后,记得更新pos结点的size值
删除函数的代码
void delet(int val)//删除权值为val的结点
{
int now = find(val);
if (!now) return;
if (cnt(now) > 1)//第一种
{
cnt(now)--, size(now)--;
return;
}
if (!ls(now) && !rs(now))//第二种
{
root = 0;
}
else if (!ls(now))//第三种
{
root = rs(root);
fa(root) = 0;
}
else if (!rs(now))//第四种
{
root