平衡树学习笔记
最近发现之前学习的平衡树已经完全写不出来了,于是赶紧重新学了一遍然后写个笔记记录下来
这里主要记录Treap和fhq Treap(我也只会这两种,Splay已经完全学不懂了)
平衡树可以说是一种优化后的二叉搜索树,因为普通的BST可能形成一条链从而使得每次操作的复杂度可能达到O(n)
所以我们需要一种方法来保证这棵树不会退化至一条链,Treap和fhq Treap都是使用的同一种方式:
给每个节点加上一个随机的优先级,然后保证树的节点权值满足BST的性质,而优先级满足Heap的性质,所以TreeHeap简称Treap就出来了
先讲讲Treap,它的主要操作是通过左旋和右旋来维护整棵树
假设有这样一颗树,节点上面的是其权值:
若要右旋这个棵树:
先将根节点的左子节点更改为根节点左子节点的右子节点:
然后根节点原来左子节点的右子节点更改为根节点:
右旋操作就完成了,而且可以看到,右旋后的树也是满足BST的
至于左旋和右旋相反,先将根节点的右子节点更改为根节点右子节点的左子节点,然后根节点原来的右子节点的左子节点更改为根节点
为了节省篇幅 (其实是懒) 我就不放图了
一颗树左旋和右旋是互逆的(大概)
左右旋的代码:
struct Tree{ //定义一个结构体来存树
int L,R; //L表示当前节点的左子节点,R表示当前节点的右子节点
int size; //size表示以当前节点为根节点的子树大小
}Treap[99];
inline void upt (int x){ //更新以x为根节点的子树大小
Treap[x].size=Treap[Treap[x].L].size+Treap[Treap[x].R].size+1;
return;
}
inline void Zig (int &x){ //右旋以x为根节点的子树
//用取地址符方便直接修改新的根节点
int z=Treap[x].L; //用z存储x的左子节点
Treap[x].L=Treap[z].R; //更新x左子节点为x左子节点的右子节点
Treap[z].R=x; //x原左子节点的右子节点更新为x
Treap[z].size=Treap[x].size; //维护子树大小
upt (x);
x=z; //当前子树根节点更新为z
return; //返回这颗新子树的根节点
}
inline void Zag (int &x){ //左旋以x为根节点的子树
//用取地址符方便直接修改新的根节点
int z=Treap[x].R; //用z存储x的右子节点
Treap[x].R=Treap[z].L; //更新x右子节点为x右子节点的左子节点
Treap[z].L=x; //x原右子节点的左子节点更新为x
Treap[z].size=Treap[x].size; //维护子树大小
upt (x);
x=z; //当前子树根节点更新为z
return; //返回这颗新子树的根节点
}
了解了Treap的基本操作后我们来看看如何将一个节点插入Treap或者从Treap中删除
首先是插入节点:
假设我们要编辑下面这棵树,不过现在我们先要在树上增加一个优先级(每个节点前面的数为节点权值,后面的数为节点的优先级):
这就是一颗正常的Treap,节点权值满足BST,优先级满足小根堆
然后假设要插入一个权值为5的节点:
首先先找到一个位置,从根节点开始:因为5>根节点2,所以找根节点的右子节点
然后因为5比根节点右儿子的权值4大,所以找当前节点的左子节点
有因为5>3,所以找当前节点的右子节点,但是右子节点为空,所以将5插入到权值为3的节点的右子节点:
2-10
/ \
/ \
/ \
1-23 4-18
/ \
/ \
/ \
3-20 7-34
\
\
\
5
画风突然简陋
然后我们给这个新节点赋予一个随机的优先级,假设为11,
2-10
/ \
/ \
/ \
1-23 4-18
/ \
/ \
/ \
3-20 7-34
\
\
\
5-11
这时就会出现一个问题:优先级不满足小根堆了,因此我们就需要之前的左旋和右旋操作,其实左右旋就是在保证子树满足BST的情况下用根节点的左右子节点代替根节点:
因为11比20小,而且这个新节点在其父节点的右子节点,所以我们需要对其父节点进行左旋操作:
2-10
/ \
/ \
/ \
1-23 4-18
/ \
/ \
/ \
5-11 7-34
\
\
\
3-20
再次检查还是不满足小根堆的结构,而且这次是新节点在其父节点的左子节点处,因此我们需要将其父节点进行右旋操作:
2-10
/ \
/ \
/ \
1-23 5-11
/ \
/ \
/ \
3-20 4-18
\
\
\
7-34
ok现在这棵树的权值满足BST结构同时优先级满足小根堆结构了,节点插入完毕
代码:
struct Tree{ //定义一个结构体来存树
int L,R; //L表示当前节点的左子节点,R表示当前节点的右子节点
int size; //size表示以当前节点为根节点的子树大小
int v; //v为当前节点的权值
int r; //r表示随机的优先级
}Treap[99];
inline void insert (int &x,int k){ //当前节点为x,插入一个权值为k的点
//用取地址符方便直接修改原值
if (!x){ //当前位置没有节点,即已经找到位置了
x=++Tot; //新建一个节点
Treap[x].v=k; //新节点的权值为k
Treap[x].r=rand (); //赋予一个随机的优先级
Treap[x].size=1; //以新节点为根节点的子树大小为1
return;
}
else ++Treap[x].size; //如果当前位置存在节点,那么新节点
//肯定在当前节点的左右子节点中
//所以直接将当前节点的子树大小加一
if (k<=Treap[x].v){ //如果k比当前节点权值小
insert (Treap[x].L,k);//向当前节点的左子节点继续搜索
if (Treap[x].r>Treap[Treap[x].L].r) //当优先级不满足小根堆时
Zig (x); //右旋当前节点
}
else { //如果k比当前节点权值大
insert (Treap[x].R,k); //向当前节点的右子节点继续搜索
if (Treap[x].r>Treap[Treap[x].R].r) //当优先级不满足小根堆时
Zag (x); //左旋当前节点
}
return;
}
而删除节点就需要进行一次讨论:
因为可能遇到两种情况:
一种是需要删除的节点只有1子节点或者没有子节点,那么就可以直接删除,只要将其父节点的左子节点/右子节点(根据需要删除的节点是其父节点的左子节点还是右子节点来定)更改为当前节点的子节点(如果没有就直接设为0):
2-10
/ \
/ \
/ \
1-23 5-11
/ \
/ \
/ \
3-20 4-18
\
\
\
7-34
比如删除权值为7的点,直接删除就好:
2-10
/ \
/ \
/ \
1-23 5-11
/ \
/ \
/ \
3-20 4-18
或者是删除权值为4的点,就将4的父节点5的右子节点设为权值为7的点就好了:
2-10
/ \
/ \
/ \
1-23 5-11
/ \
/ \
/ \
3-20 7-34
但是若我们需要删除权值为5的节点,那上面的方法就会有些无力,但是我们可以尝试通过旋转来解决这个难题,因为旋转会让一颗子树的根节点向下移动,只要我们把它移动到一个可以直接删除的地方然后直接删除就好了:
先看其两个子节点的优先级,应该将优先级小的旋转上来以维持小根堆的结构,这里我们通过左旋来把权值为5的点的右儿子旋转上来:
2-10
/ \
/ \
/ \
1-23 4-18
/ \
/ \
/ \
5-11 7-34
/
/
/
3-20
这时权值为5的节点只有一个子节点了,我们可以直接删除:
2-10
/ \
/ \
/ \
1-23 4-18
/ \
/ \
/ \
3-20 7-34
以下是代码:
struct Tree{ //定义一个结构体来存树
int L,R; //L表示当前节点的左子节点,R表示当前节点的右子节点
int size; //size表示以当前节点为根节点的子树大小
int v; //v为当前节点的权值
int r; //r表示随机的优先级
}Treap[99];
inline void Delete (int &x,int k){ //x为当前节点位置 k为需要删除的权值的节点
//用取地址符方便修改x父节点的子节点
if (Treap[x].v==k){ //找到需要删除的节点了
if (!Treap[x].L||!Treap[x].R) x=Treap[x].L+Treap[x].R; //第一种情况可以一起处理
//以为若该节点无子节点,x就会等于0
//若只有一个子节点,x就会变为他的存在的那个子节点
else if (Treap[Treap[x].L].r<Treap[Treap[x].R].r){ //当前节点左子节点的优先级比较小的情况
Zig (x); //右旋当前节点
Delete (x,k); //因为x被旋下去了所以继续向下找直到可以删除x这个节点
}
else { //当前节点左子节点的优先级比较大的情况
Zag (x); //左旋当前节点
Delete (x,k); //因为x被旋下去了所以继续向下找直到可以删除x这个节点
}
return;
}
Treap[x].size--; //维护子树大小,因为需要删除的节点一定在当前节点的子节点上
if (k<=Treap[x].v) //如果k比当前节点权值小
Delete (Treap[x].L,K); //向左寻找要删除节点
else //如果k比当前节点权值大
Delete (Treap[x].R,K); //向右寻找要删除节点
return;
}
最后的几个操作:查询排名和前后驱
因为Treap满足BST的性质,既其左子树的权值都比其本身权值小
那么如果要查询权值为k的元素的排名:
- 首先从根节点开始查找,同时用一个变量ans记录结果,如果查询权值等于根节点权值就直接返回根节点的左子树大小再加一(因为根节点左边的节点必定比查询权值小,再加一是因为要求排名)
- 如果查询权值小于根节点权值那么就向根节点左边进行查找,如果大于根节点权值就从根节点右边开始查找,ans加上根节点的左子树大小再加一(因为根节点和根节点左子树的所有元素必定比k小)
- 然后把查找到的节点继续按上面的步骤走直到找到查询节点
代码也不是很复杂:
inline int rank (int k){ //查询权值为k的排名
int ans=0,x=root; //ans为比k小的节点数 root为构造的Treap的根节点
while (x){ //当x不为0时即还可以找到节点
if (k==Treap[x].v) //找到节点了
return ans+Treap[Treap[x].L].size+1; //返回当前节点右子节点的子树大小加上
//之前比查询权值大的节点数加一
else if (k<Treap[x].v) //如果k比当前节点权值小
x=Treap[x].L; //向当前节点的左子节点进行查找,
//因为当前节点比查询权值大的话无法对答案做出任何贡献
else { //如果k比当前节点权值大
ans+=Treap[Treap[x].L].size+1; //比k小的节点数就增加了
//当前节点(1)和其左子树大小(Treap[Treap[x].L].size)
x=Treap[x].R //继续向左寻找
}
}
return 0; //如果不存在这个节点返回0(不一定为0,按照自己需求进行修改)
}
如果我们要查找排名为k的节点权值:
- 和上面类似,首先还是从根节点开始查找,如果k等于根节点左子树大小加一,那么返回根节点的权值,因为根节点的左边节点全部比根节点权值小,右边节点全部比根节点权值大,所以根节点的排名就是其左子树的大小加一
- 如果k比根节点权值小,那么向根节点的左子树继续搜索,如果k比根节点权值大,那么向根节点的右子树进行搜索,但是同时k要减去当前节点的左子树大小加一,因为它在右子树的排名不可能还是k了,而是还要减去当前节点左子树大小加一...(这个慢慢理解就好,开始我在这里打了一大段文字想要解释然后把我自己绕晕了XD)
- 然后重复上面的过程直到找到排名为k的节点
代码和上一种有点像:
inline int rank (int k){ //查询排名为k的节点权值大小
int x=root; //root为构造的Treap的根节点
while (x){ //当x不为0时即还可以找到节点
if (k==Treap[Treap[x].L].size) //找到节点了
return Treap[x].v; //返回当前节点的权值
else if (k<=Treap[Treap[x].L].size) //如果k的排名比当前节点右子树大小小
x=Treap[x].L; //向当前节点的左子节点进行查找,
else { //如果k的排名比当前节点右子树大小大
k-=(Treap[Treap[x].L].size+1) //k要减去当前节点左子树大小+1
x=Treap[x].R //继续向左寻找
}
}
return 0; //如果不存在这个节点返回0(不一定为0,按照自己需求进行修改)
}
至于查询前驱后继(查询k的前驱就是查询在Treap中不大于该元素的最大元素,后继则相反):
以查询前驱为例:
- 其实还是差不多那么几个步骤,从根节点开始,如果当前节点的权值小于查询权值,更新答案为当前节点的权值并继续向右查找
- 如果当前节点权值小于查询权值,向左继续查找
- 重复步骤直到当前节点为空,返回答案
代码:
inline int QueryPre (int k){ //查询前驱
int x=root,ans=-2147483647; //x为根节点,ans为答案,初始化为极小值是为了防止没有找到前驱
while (x){ //循环至空的节点
if (Treap[x].v<=k){ //如果当前节点比k小
ans=Treap[x].v; //更新答案
x=Treap[x].R; //继续向左查找
}
else x=Treap[x].L; //如果当前节点比k大则不更新
//向左查找直到找到权值比k小的节点再更新
}
return ans; //返回答案
}
inline int QuerySuf (int k){ //查询后继,同上
int x=root,ans=2147483647;
while (x){
if (Treap[x].v>=k){
ans=Treap[x].v;
x=Treap[x].L;
}
else x=Treap[x].R;
}
return ans;
}
最后提一点,如果存在重复节点右两种处理方法:
一种是当做两种节点
另一种是再在结构体里加个变量,用来存当前节点的个数,然后其他地方再进行点处理就ok了
OK来更新fhq Treap了
这种平衡树的也是通过增加一个优先级来进行优化,但是对于与普通Treap,fhq没有所谓的旋转操作,有的是另外两种操作——分裂与合并
所谓分裂,一般是指权值分裂(还有一种叫排名分裂但是我不会。。。),这种分裂会将Treap按权值k分为两棵树,权值小于等于k的分到左边的树,权值大于k的分到右边的树
例如有这样一棵树:
5
/ \
/ \
/ \
3 7
/ \ / \
/ \ / \
/ \ / \
1 4 6 9
你以为我会改变画风?怎么可能!
若我们按权值6进行权值分裂:
· 首先从根节点开始遍历:因为6>5所以分裂出来的左边的树的根节点为5,同时5的左子树全部归到左边的树中,然后向右遍历
· 然后因为6<7所以分裂出来的右边的树的根节点为7,同时7的右子树全部归到右边的树种,然后向左遍历
· 最后因为6=6所以把6归到左树中,父节点为左子树根节点的左子节点,然后6的左子树归到左树中,然后向右遍历
· 因为当前节点为空,遍历完毕,整棵树已经按权值分为了两棵树:
5 7
/ \ \
/ \ \
/ \ \
3 6 9
/ \
/ \
/ \
1 4
代码即解释如下:
struct node{ //用结构体存树
int L,R; //L,R分别为当前节点的左/右节点编号
int k; //k为当前节点权值
int r; //r为随机的优先级
int size; //size当前节点的子树大小
}treap[2020022];
inline void del (int x){ //调整子树大小
treap[x].size=treap[treap[x].R].size+treap[treap[x].L].size+1;
return;
}
inline void Split (int v,int k,int &L,int &R){ //v表示当前节点,k为分裂权值
//L表示可以添加到左树的位置
//R表示可以添加到右树的位置
//用取地址符来直接修改节点
if (!v){ //当当前节点为空节点时,左右树的可添加节点也变为空节点
L=R=0;
return;
}
if (treap[v].k<=k){ //当当前节点权值小于等于分裂权值k时
L=v; //将当前节点放到左树上可添加节点的位置
Split (treap[v].R,k,treap[v].R,R); //同时继续向右分裂,R不变
//但是左树可以更新的节点变为了当前节点的右子节点
//因为当前节点和其左子树都到了左树
//所以当前节点的右子节点空了出来= =
del (v); //调整子树大小,这个和Treap的函数是一样
return;
}
else if (treap[v].k>k){ //当当前节点权值大于分裂权值k时
R=v; //将当前节点放到右树上可添加节点的位置
Split (treap[v].L,k,L,treap[v].L); //同时继续向左分裂,L不变
//但是右树可以更新的节点变为了当前节点的左子节点
//理由同上
del (v); //调整子树大小
return;
}
}
Spilt (root,k,l,r); //按权值k将根节点为root的树分为两棵树
//左边树的根节点为l,右边树的根节点为r
有分必然有和,因此我还得降他们合并起来:
从左右树的根开始,比较他们的优先级,如果左树的根的优先级比较小,就保留左边的树的根节点和其左子树,然后合并其右子树和右树,其右子节点为合并其右子树和右树后的树的根节点
若左树的根的优先级比较大,就保留右树的根节点和其右子树,然后继续将其左子树和左树合并,其左子节点为合并其左子树和左树后的树的根节点
直到合并的两个根其中至少有一个空节点时返回那个非空节点,若都为空直接返回空节点
代码及解释如下:
struct node{ //用结构体存树
int L,R; //L,R分别为当前节点的左/右节点编号
int k; //k为当前节点权值
int r; //r为随机的优先级
int size; //size当前节点的子树大小
}treap[2020022];
inline int merge (int TA,int TB){ //合并根节点为TA和根节点为TB的树
if (!TA||!TB) return TA+TB; //当其中至少一个为空节点时返回那个非空节点
//若都为空节点直接返回空节点
if (treap[TA].r<treap[TB].r){ //比较根节点优先级大小,如果TA的优先级小时
treap[TA].R=merge (treap[TA].R,TB); //根节点为TA的右子节点更新为
//合并其右子树和根节点为TB的树 后的根节点
del (TA); //调整子树大小
return TA;
}
else{ //若TA优先级较大时
treap[TB].L=merge (TA,treap[TB].L); //根节点为TB的左子节点更新为
//合并根节点为TA的树和其左子树 后的根节点
del (TB); //调整子树大小
return TB;
}
}
OK现在我们已经会了fhq Treap的基本的两个操作,接下来就是通过这两个操作实现一棵平衡树:
操作1_元素的插入:
例如我们需要在下面这棵树中插入一个权值为8的元素:
5
/ \
/ \
/ \
3 7
/ \ / \
/ \ / \
/ \ / \
1 4 6 9
那么首先按照插入权值8将树进行分裂,同时用全局变量LT和RT存储分裂后左树和右树的根节点:
分裂后的树应该是这样的:
5 9
/ \
/ \
/ \
3 7
/ \ /
/ \ /
/ \ /
1 4 6
然后以权值8新建一个节点,同时节点数加一:
5 9 8
/ \
/ \
/ \
3 7
/ \ /
/ \ /
/ \ /
1 4 6
合并左树和新的节点:
5 9
/ \
/ \
/ \
3 7
/ \ / \
/ \ / \
/ \ / \
1 4 6 8
用合并后的左树合并右树:
5
/ \
/ \
/ \
3 7
/ \ / \
/ \ / \
/ \ / \
1 4 6 8
\
\
\
9
(最后的树不一定是这样的,因为还要考虑优先级的关系,因为我懒就没考虑随便找了个地方插入)
这样就将一个节点插入到了我们的平衡树中:
操作代码如下:
struct node{ //用结构体存树
int L,R; //L,R分别为当前节点的左/右节点编号
int k; //k为当前节点权值
int r; //r为随机的优先级
int size; //size当前节点的子树大小
}treap[2020022];
int root,l,r,Treap_size; //root为整棵平衡树的根节点编号
//l,r分别为分裂后的左右树根节点编号
//Treap_size为节点数
inline void newnode (int x){ //新建一个节点,节点数加一
treap[++Treap_size].size=1; //其子树大小为一
treap[Treap_size].k=x; //权值为插入节点权值
treap[Treap_size].r=rand(); //赋予随机的优先级
return;
}
inline void Insert (int x){ //插入权值为x的点
split (root,x,l,r); //按权值x分裂整棵树
newnode (x); //新建权值为x的节点
root=merge (l,Treap_size); //合并左树和新节点
root=merge (root,r); //合并上面合并后的树和右树
return;
}
操作10_元素的删除:
还是熟悉的这棵树:
5
/ \
/ \
/ \
3 7
/ \ / \
/ \ / \
/ \ / \
1 4 6 8
\
\
\
9
若需要删除一个权值为7的点,这个就不需要像之前一样讨论情况了,因为我们有着分裂操作,只需要将这个节点分裂出来就好了
那么具体怎么操作呢:
首先先把整棵树按照需要删除的权值7分裂成两棵树,这个时候我就不用左树和右树了,称为一树和二树,
分裂后应该是这样的(大概):
5 8
/ \ \
/ \ \
/ \ \
3 7 9
/ \ /
/ \ /
/ \ /
1 4 6
但是,此时权值为7的点还没有分离出来,我们再对一树进行一次分裂:
将一树按要删除的权值减一分裂成两棵树,称为一树和三树:
5 7 8
/ \ \
/ \ \
/ \ \
3 6 9
/ \
/ \
/ \
1 4
先解释一下为什么是按照要删除的权值减一来分裂:
我们需要将一树分裂成两棵树使得要删除的权值的节点在某一棵树的根节点上(如果不是根节点那就不好处理,到最后还是得成为某棵树的根节点,还不如现在就分出来)而选择分裂一树的原因是我们的分裂函数是按照权值分裂,分裂到左树的条件是小于等于分裂权值,而分裂到右树的条件是大于分裂权值,如果前面条件是小于而后面条件是大于等于你也可以分裂二树,而按删除权值减一就是因为这里有个等于,这样可以保证权值为删除权值的节点一定在分裂后的右边树的根节点(如果你要想分裂右边的树,那么这里也要改成权值加一
(然后感觉解释了一大堆我自己都没搞清楚)
然后合并新分裂出来的三树的左右子树成为新的三树,因为三树的根节点肯定是需要删除的节点(除非树中不存在这个节点,那种题目就需要进行判断了),这样合并后这个节点就和整棵树无关了,也就是删除了
然后依次合并剩下的三棵树就好啦
代码:
inline void Delete (int x){ //删除权值为x的节点
int TC=0; //TC存三树的根节点
Split (root,x,l,r); //先按权值x分裂整棵树为一二树
//l为一树根节点编号,r为二树根节点编号
Split (l,x-1,l,TC); //然后按权值x-1分裂一树为一三树
//l为新的一树的根节点编号,TC为三树的根节点编号
TC=merge (treap[TC].L,treap[TC].R); //然后先将TC的左右子树合并,将TC节点删除
root=merge (l,TC); //合并一三树
root=merge (root,r); //合并剩下的两棵树
return;
}
操作11_查询排名:
查询排名和上面一样分为两种,一种是权值为k的元素的排名,一种是排名为k的元素的权值:
先讲第一种,方法很简单,按照之前删除的操作,将整棵树按查询权值k-1分裂成两棵树,这样就可以保证左树全部都小于权值k,那么答案就是左树的子树大小+1(因为左树的子树大小只是小于k的元素个数,求排名还是要加一),最后不要忘记将分裂出来的树进行合并
代码:
inline int getrank (int k){ //查询权值为k的元素的排名
Split (root,k-1,l,r); //按权值为k-1分裂为两棵子树,那么左树的节点权值必定小于k
int res=treap[l].size+1; //答案就是左树的大小加一
root=merge (l,r); //不要忘记合并
return res;
}
然后是查询排名为k的元素权值
这个可以说和之前的Treap的的查询方法是一样的
inline int Kth (int v,int k){ //几乎一模一样的代码,我也不多做注释了
//只是注意v的意思是在以v为根节点的树中进行查找
while (v){
if (k<=treap[treap[v].L].size)
v=treap[v].L;
else if (k==treap[treap[v].L].size+1)
return treap[v].k;
else {
k-=(treap[treap[v].L].size+1);
v=treap[v].R;
}
}
return 0;
}
操作100_查询前驱/后继:
这里大可以用之前的方法做,但是这里介绍个更好的方法:
我们之前已经学会让整棵树按照k-1的大小分裂成两棵树使得左树的权值都会小于k,那么左树中最大的不就是k的前驱了吗?我们只需要一次分裂然后在左树中查询最大的元素,即左树中排名为左树大小的元素,然后再进行合并就好了,甚至可以不用写其他的函数
查询后继也是差不多的道理,按k的大小分裂成两棵树,这样右树的所有权值一定大于k,然后我们只要在右树中查询最小的元素,即右树中排名为1的元素,然后合并就OK啦:
还是放一下代码:
inline int QueryPre (int k){ //查询前驱
Split (root,k-1,l,r);
int res=Kth (l,treap[l].size);
root=merge (l,r);
return treap[res].k; //返回答案
}
inline int QuerySuf (int k){ //查询后继,同上
Split (root,k,l,r);
int res=Kth (r,1);
root=merge (l,r);
return treap[res].k; //返回答案
}