2023.3.10 【数据结构】普通平衡树
2023.3.10 【模板】普通平衡树
推荐一篇写平衡树写的很好的博客:算法学习笔记(18): 平衡树(一) - jeefy - 博客园 (cnblogs.com)
问题陈述
写一种数据结构,支持以下六种操作:
1.插入一个数x
2.删除一个数x
3.查询x的排名(比x小的数 + 1)
4.查询排名为x的数
5.查询x的前驱
6.查询x的后继
这种操作可以用一个叫二叉查找树(BST)的东西实现,这玩意有以下性质:
翻译过来,就是一个节点左子树的值小于这个节点,右子树的值大于这个节点
这样在理想状态下,就可以每次从树根开始,实现这个问题,设操作数为Q,则理论时间复杂度为
但是会有(奇葩的)数据,使得构造的树长成这个样子:
这样深度变成了
基于这一点,我们可以考虑将这个不平衡的树变成平衡的(大体平衡的就行)。
总体上考虑随机算法。
以下将呈现三种平衡树:
Treap
Treap,意为Tree + Heap,字面理解,就是这棵树既具有堆的性质,又具有BST的性质。
如何做到呢?我们在每个节点上给它赋一个随机值Heap,依照Heap值的性质来进行操作。
我们可以发现,上面那棵树其实有好几种形态,而且都具有BST的性质:
可以观察到,这棵树像是一根悬挂在螺丝钉上的链子一样,可以往左拉,也可以往右拉。我们称之为”旋转“操作,这是平衡树最重要的操作之一。而”旋转“操作又分为”左旋“和”右旋“。
”左旋“ (Zag)
左旋,即”向左旋转“,即向左拉这条链子,具体些,我们使用一张图:
观察上图,我们发现有三个点的父、子关系发生了变化:1(grandfa)、3(fa)、4(grandson左) 和 5(son),(以下简称为gf,f,s,gs)
按照如下顺序改变关系:
1.f的右儿子 = gs
2.gs == 0 ? NULL : gs的父亲 = f
3.s的左儿子 = f
4.f的父亲 = s
5.gf == 0 ? 根 = s : gf相应方向的儿子 = s
6.s的父亲 = gf
改变后不要忘记维护值
至于那个”相应方向的儿子“,可以用一个get()函数来维护
inline int get(int x)//0左1右
{
return x == t[t[x].fa].rc;
}
t[f].rc = t[s].lc;
t[t[s].lc].fa = f;
t[s].lc = f;
if(gf != 0)
(get(f) ? t[gf].rc : t[gf].lc) = s;
else
root = s;
t[s].fa = gf;
t[f].fa = s;
maintain(x);
maintain(y);
if(z)
maintain(z);
右旋(Zig)
同理,只是将方向改变一下即可:(以下gs为grandson右)
1.f的左儿子 = gs
2.gs == 0 ? NULL : gs的父亲 = f
3.s的右儿子 = f
4.f的父亲 = s
5.gf == 0 ? 根 = s : gf相应方向的儿子 = s
6.s的父亲 = gf
t[f].lc = t[s].rc;
t[t[s].rc].fa = f;
t[s].rc= f;
if(gf != 0)
(get(f) ? t[gf].rc : t[gf].lc) = s;
else
root = s;
t[s].fa = gf;
t[f].fa = s;
maintain(x);
maintain(y);
if(z)
maintain(z);
为了方便,以上两个函数可以合并成一个函数rorate()
插入
首先按照BST的性质造点,在判断如果子节点的Heap值小于父节点的Heap值,就旋转子节点
删除
将这个点的两个儿子中Heap值小的旋转上来,直到这个点成为叶节点,删除这个点(直接断开它的father与它的联系即可)。注意删除后要沿着父亲这条链一路向上更新。
查找x的排名
直接找到比这个数小的数,然后+1即可
inline int Get_Rank_By_Val(int k,int x)
{
if(x == 0) return 1;
if(t[x].val == k) return t[t[x].lc].siz + 1;
if(t[x].val < k) return t[x].cnt + t[t[x].lc].siz + Get_Rank_By_Val(k,t[x].rc);
if(t[x].val > k) return Get_Rank_By_Val(k,t[x].lc);
}
查找排名为x的数
如果当前排名在左子树大小 + 1 和 左子树大小 + 当前节点重复数 之间,就返回当前排名
inline int Get_Val_By_Rank(int k,int x)
{
if(t[t[x].lc].siz + 1 <= k && k <= t[t[x].lc].siz + t[x].cnt) return t[x].val;
if(k <= t[t[x].lc].siz)
return Get_Val_By_Rank(k,t[x].lc);
else
return Get_Val_By_Rank(k - t[t[x].lc].siz - t[x].cnt,t[x].rc);
}
查找前驱
如果当前数大于等于k,就向左子树寻找,否则就记录下答案,然后找右子树即可
inline int Getpre(int k,int x)
{
if(x == 0) return -0x7f7f7f7f;
if(t[x].val >= k)
return Getpre(k,t[x].lc);
int maxi = -0x7f7f7f7f;
if(t[x].val < k)
maxi = max(maxi,Getpre(k,t[x].rc));
maxi = max(maxi,t[x].val);
return maxi;
}
查找后继
同理
inline int Getpost(int k,int x)
{
if(x == 0) return 0x7f7f7f7f;
if(t[x].val <= k)
return Getpost(k,t[x].rc);
int mini = 0x7f7f7f7f;
if(t[x].val > k)
mini = min(mini,Getpost(k,t[x].lc));
mini = min(mini,t[x].val);
return mini;
}
这样,我们就完成了Treap的编写。
Splay
Splay树最有特色的就是它的Splay函数,这个函数的作用是将当前节点一直转到根。通过将很多节点随机旋转到根的做法保持平衡,删除的时候还是将它旋转到叶节点
Splay完整代码:
#include<bits/stdc++.h>
using namespace std;
const int N = 1e5 + 5;
struct Splay_Tree{
int lc,rc,siz,fa,val,cnt;
}t[N];
int n,root,tot = 0;
inline int maintain(int x)
{
t[x].siz = t[t[x].lc].siz + t[t[x].rc].siz + t[x].cnt;
}
inline int get(int x)//0左1右
{
return x == t[t[x].fa].rc;
}
inline void rorate(int x)
{
int y = t[x].fa,z = t[y].fa;
if(!get(x))
{
t[y].lc = t[x].rc;
t[t[x].rc].fa = y;
t[x].rc = y;
if(z != 0)
(get(y) ? t[z].rc : t[z].lc) = x;
else
root = x;
t[x].fa = z;
t[y].fa = x;
}
else
{
t[y].rc = t[x].lc;
t[t[x].lc].fa = y;
t[x].lc = y;
if(z != 0)
(get(y) ? t[z].rc : t[z].lc) = x;
else
root = x;
t[x].fa = z;
t[y].fa = x;
}
maintain(y);
maintain(x);
if(z != 0)
maintain(z);
}
inline void splay(int x)
{
while(t[x].fa != 0)
{
if(t[x].fa == root)
rorate(x);
else if(get(x) == get(t[x].fa))
rorate(t[x].fa),rorate(x);
else
rorate(x),rorate(x);
}
root = x;
}
inline void upd_line(int x)
{
maintain(x);
if(t[x].fa != 0)
upd_line(t[x].fa);
}
inline void insert(int k,int x)
{
if(t[x].val == 0)
{
if(root == 0)
{
root = ++tot;
x = root;
}
t[x].cnt++;
t[x].val = k;
upd_line(x);
splay(x);
return;
}
if(t[x].val == k)
{
++t[x].cnt;
upd_line(x);
splay(x);
return;
}
if(k < t[x].val)
{
if(t[x].lc == 0)
{
t[x].lc = ++tot;
t[t[x].lc].fa = x;
}
insert(k,t[x].lc);
}
else
{
if(t[x].rc == 0)
{
t[x].rc = ++tot;
t[t[x].rc].fa = x;
}
insert(k,t[x].rc);
}
maintain(x);
}
inline int Get_Num_By_Val(int k,int x)
{
if(k == t[x].val) return x;
if(k < t[x].val)
{
if(t[x].lc != 0)
return Get_Num_By_Val(k,t[x].lc);
else
return x;
}
else
{
if(t[x].rc != 0)
return Get_Num_By_Val(k,t[x].rc);
else
return x;
}
}
inline int Get_Rank_By_Val(int k,int x)
{
if(x == 0) return 1;
if(t[x].val == k) return t[t[x].lc].siz + 1;
if(t[x].val < k) return t[x].cnt + t[t[x].lc].siz + Get_Rank_By_Val(k,t[x].rc);
if(t[x].val > k) return Get_Rank_By_Val(k,t[x].lc);
}
inline int Get_Val_By_Rank(int k,int x)
{
if(t[t[x].lc].siz + 1 <= k && k <= t[t[x].lc].siz + t[x].cnt) return t[x].val;
if(k <= t[t[x].lc].siz)
return Get_Val_By_Rank(k,t[x].lc);
else
return Get_Val_By_Rank(k - t[t[x].lc].siz - t[x].cnt,t[x].rc);
}
inline int Getpre(int k,int x)
{
if(x == 0) return -0x7f7f7f7f;
if(t[x].val >= k)
return Getpre(k,t[x].lc);
int maxi = -0x7f7f7f7f;
if(t[x].val < k)
maxi = max(maxi,Getpre(k,t[x].rc));
maxi = max(maxi,t[x].val);
return maxi;
}
inline int Getpost(int k,int x)
{
if(x == 0) return 0x7f7f7f7f;
if(t[x].val <= k)
return Getpost(k,t[x].rc);
int mini = 0x7f7f7f7f;
if(t[x].val > k)
mini = min(mini,Getpost(k,t[x].lc));
mini = min(mini,t[x].val);
return mini;
}
inline void del(int k,int x)
{
x = Get_Num_By_Val(k,root);
if(x == -1) return;
if(t[x].cnt > 1)
{
--t[x].cnt;
upd_line(x);
splay(x);
return;
}
while(t[x].lc != 0 || t[x].rc != 0)
{
if(t[x].lc != 0)
rorate(t[x].lc);
else
rorate(t[x].rc);
}
if(t[x].fa != 0)
{
(get(x) ? t[t[x].fa].rc : t[t[x].fa].lc) = 0;
upd_line(t[x].fa);
}
else
root = 0;
}
int main()
{
cin>>n;
int op,x;
for(int i = 1;i <= n;i++)
{
cin>>op>>x;
switch(op)
{
case 1:
insert(x,root);break;
case 2:
del(x,root);break;
case 3:
cout<<Get_Rank_By_Val(x,root)<<endl;splay(Get_Num_By_Val(x,root));break;
case 4:
cout<<Get_Val_By_Rank(x,root)<<endl;break;
case 5:
cout<<Getpre(x,root)<<endl;splay(Get_Num_By_Val(x,root));break;
case 6:
cout<<Getpost(x,root)<<endl;splay(Get_Num_By_Val(x,root));break;
}
}
}
乍一看,含有”旋转“的平衡树写起来都稍显麻烦,那么有什么方法能让我们不旋转也能实现平衡树呢?
FHQ-Treap
详见《范浩强谈数据结构》一书
FHQ-Treap,属于非旋Treap之一。有两个核心操作:分裂(split) 和 合并(merge)。一般所采取的分裂是按Tree值分裂,而合并是按Tree和Heap两个值合并。
Split
(采用OI-Wiki上面的图)
我们采用引用调用,在每走到一个点时,如果这个点小于等于key的值,我们就将这个节点归为左树,然后去分割它的右子树,反之亦然。这样到叶节点时就会将这棵树彻底剖开。
同时我们就不用将Tree值相同的节点放在一个点里面,而是分为多个点就好了。
inline void split(int &x,int &y,int now,int k)
{
if(!now)
{
x = y = 0;
return;
}
if(t[now].val <= k)
x = now,split(t[now].rc,y,t[now].rc,k);
else
y = now,split(x,t[now].lc,t[now].lc,k);
update(now);
}
Merge
分裂后的两棵树有一个极好的性质:左子树一定小于右子树。那么在合并的时候,我们就可以充分利用这个性质。我们利用x树小于y树的性质,所以要么x树和y树的左子树合并,要么y树和x树的右子树合并,至于选哪个,就按照Heap值判断即可。
inline int merge(int x,int y)//默认 tree[x] < tree[y]
{
if(!x || !y) return x + y;
if(t[x].hp < t[y].hp)
{
t[x].rc = merge(t[x].rc,y);
update(x);
return x;
}
else
{
t[y].lc = merge(x,t[y].lc);
update(y);
return y;
}
}
Insert
将整棵树按照x值分裂,将小树与x合并,再与大树合并即可。
inline int new_node(int x)
{
++tot;
t[tot].lc = t[tot].rc = 0;
t[tot].val = x;
t[tot].hp = rand();
t[tot].siz = 1;
return tot;
}
inline void insert(int x)
{
int y,z;
split(y,z,root,x);
root = merge(merge(y,new_node(x)),z);
}
Delete
将整棵树按照x值分裂,将小树按照x - 1分裂,为了防止等于x值的树(简称x树),将x定义为x的左儿子和右儿子合并即可,在分别将x - 1树,x树,大树合并。
inline void del(int x)
{
int y,z,w,p;
split(y,z,root,x);
split(w,p,y,x - 1);
p = merge(t[p].lc,t[p].rc);
root = merge(merge(w,p),z);
}
求x的排名
将整棵树按照x - 1分裂,然后求小树的大小 + 1即可
inline int Get_Rank_By_Val(int x)
{
int y,z;
split(y,z,root,x - 1);
int ret = t[y].siz + 1;
root = merge(y,z);
return ret;
}
求x的前驱
将整棵树按照x - 1分裂,然后取小树中的最大值即可
inline int Getpre(int x)
{
int y,z;
split(y,z,root,x - 1);
int ret = t[kth(t[y].siz,y)].val;
root = merge(y,z);
return ret;
}
求x的后继
按照x + 1分裂,然后求大树中的最小值即可
inline int Getpost(int x)
{
int y,z;
split(y,z,root,x);
int ret = t[kth(1,z)].val;
root = merge(y,z);
return ret;
}
Code
#include<bits/stdc++.h>
using namespace std;
const int N = 1e5 + 5;
struct fhq_treap{
int lc,rc,val,hp,siz;
}t[N];
int root = 0,tot = 0;
inline void update(int pos)
{
t[pos].siz = t[t[pos].lc].siz + t[t[pos].rc].siz + 1;
}
inline void split(int &x,int &y,int now,int k)
{
if(!now)
{
x = y = 0;
return;
}
if(t[now].val <= k)
x = now,split(t[now].rc,y,t[now].rc,k);
else
y = now,split(x,t[now].lc,t[now].lc,k);
update(now);
}
inline int merge(int x,int y)//默认 tree[x] < tree[y]
{
if(!x || !y) return x + y;
if(t[x].hp < t[y].hp)
{
t[x].rc = merge(t[x].rc,y);
update(x);
return x;
}
else
{
t[y].lc = merge(x,t[y].lc);
update(y);
return y;
}
}
inline int new_node(int x)
{
++tot;
t[tot].lc = t[tot].rc = 0;
t[tot].val = x;
t[tot].hp = rand();
t[tot].siz = 1;
return tot;
}
inline void insert(int x)
{
int y,z;
split(y,z,root,x);
root = merge(merge(y,new_node(x)),z);
}
inline void del(int x)
{
int y,z,w,p;
split(y,z,root,x);
split(w,p,y,x - 1);
p = merge(t[p].lc,t[p].rc);
root = merge(merge(w,p),z);
}
inline int Get_Rank_By_Val(int x)
{
int y,z;
split(y,z,root,x - 1);
int ret = t[y].siz + 1;
root = merge(y,z);
return ret;
}
inline int kth(int x,int pos)
{
if(x == t[t[pos].lc].siz + 1) return pos;
else if (x > t[t[pos].lc].siz + 1) return kth(x - t[t[pos].lc].siz - 1,t[pos].rc);
else return kth(x,t[pos].lc);
}
inline int Getpre(int x)
{
int y,z;
split(y,z,root,x - 1);
int ret = t[kth(t[y].siz,y)].val;
root = merge(y,z);
return ret;
}
inline int Getpost(int x)
{
int y,z;
split(y,z,root,x);
int ret = t[kth(1,z)].val;
root = merge(y,z);
return ret;
}
int main()
{
srand(time(NULL));
int n,opt,x;
cin>>n;
for(int i = 1;i <= n;i++)
{
cin>>opt>>x;
switch(opt)
{
case 1:
insert(x);break;
case 2:
del(x);break;
case 3:
cout<<Get_Rank_By_Val(x)<<endl;break;
case 4:
cout<<t[kth(x,root)].val<<endl;break;
case 5:
cout<<Getpre(x)<<endl;break;
case 6:
cout<<Getpost(x)<<endl;break;
}
}
return 0;
}
完结撒花
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现
· 25岁的心里话