第1节 平衡树的引入
在上一讲中提到,由于二叉排序树的复杂度很容易退化,因此在实际中的用途没有那么广。但是如果二叉排序树能实现平衡,那么二叉排序树的时间复杂度为,非常优秀,可以得到非常广泛的应用。
这里所谓的“平衡”,在不同的地方有着不同的定义, 常见的有高度平衡树、重量平衡树等。但其主要目标,都是保证结点最大深度不超过,以实现高效的查找效率。
在接下来的几节中,我们将通过下面的两道例题,来依次介绍替罪羊树、Treap、Splay等一些常用的平衡方法。
您需要写一种数据结构(可参考题目标题),来维护一个数集,其中需要提供以下操作:
1.插入一个整数x。
2.删除一个整数x(若有多个相同的数,只删除一个)。
3.查询整数x的排名(排名定义为比当前数小的数的个数+1)。
4.查询排名为x的数。
5.求x的前驱(前驱定义为小于x且最大的数)。
6.求x的后继(后继定义为大于x且最小的数)。
保证所有操作合法。
在上一题的基础上,扩大数据范围并增加了强制在线。
第2节 替罪羊树
替罪羊树的原理在上一讲中已经做了详细说明,这里不再赘述。下面介绍替罪羊树的实现:
3.2.1 替罪羊树的结点
struct node
{
int val,cnt,tot;
node *lc,*rc;
node(int _val=0,int _cnt=0,int _tot=0)
{
val=_val,cnt=_cnt,tot=_tot;
lc=rc=NULL;
}
}*root,*null,**goat;
其中,val代表该结点中的数据(权值),cnt代表此数据出现次数,tot代表以该结点为根的子树大小(每个结点记cnt次)。
lc和rc代表该结点的左右儿子。
root代表根结点,null代表“空”结点(即一个所有变量均为0的结点,注意与NULL的区别),goat代表替罪羊结点(即深度最小的不平衡的结点,用于重构)。
3.2.2 替罪羊树的重构
首先,我们需要判定一个结点是否应重构。为此,我们引入一个平衡因子(取值在(0.5,1),一般采用0.7或0.8),若某结点的子结点大小占它本身大小的比例超过
,则该结点是不平衡的,应当重构。
另外,为了避免一些不必要的重构(例如某结点的儿子是不平衡的,重构完又发现该结点还是不平衡的,于是再次重构,而事实上只需重构该结点即可),我们引入替罪羊结点goat。goat表示深度最小的不平衡的结点,这样我们只需对goat进行重构即可。
void update(node* &now)
{
now->tot=now->cnt+now->lc->tot+now->rc->tot;
if(std::max(now->lc->tot,now->rc->tot)>alpha*now->tot)
goat=&now;
}
值得注意的是,这里选取子结点的tot(每个结点记cnt次)作为子结点大小,从而判断该结点是否平衡。事实上,还可以选取csiz(每个结点记1次)作为子结点大小,而且这种选取方式显得更加直观。可以证明,这两种选取方式均可保证树高是log级别的,具体证明留给读者思考。
重构分为两步——unfold(按中序遍历展开并存入数组)和rebuild(二分重建成树)。
void balance(node* &now)
{
vector<node*> v;
unfold(v,now);
now=rebuild(v,0,v.size()-1);
}
void unfold(vector<node*> &v,node *now)
{
if(now==null)
return;
unfold(v,now->lc);
if(now->cnt)
v.push_back(now);
unfold(v,now->rc);
if(!now->cnt)
delete now;
}
node* rebuild(vector<node*> &v,int l,int r)
{
if(l>r)
return null;
int mid=l+r>>1;
v[mid]->lc=rebuild(v,l,mid-1);
v[mid]->rc=rebuild(v,mid+1,r);
update(v[mid]);
return v[mid];
}
3.2.3 替罪羊树的插入
插入时,到达“空”结点则新建结点,找到对应结点则cnt++。return后,再update该结点即可(update已包含对goat的更新)。
void __insert(node* &now,int x)
{
if(now==null)
{
now=new node(x,1,1);
now->lc=now->rc=null;
return;
}
if(x==now->val)
now->cnt++;
else if(x<now->val)
__insert(now->lc,x);
else
__insert(now->rc,x);
update(now);
}
3.2.4 替罪羊树的删除
删除时,我们可以采用惰性删除,找到对应结点时直接cnt--,实际上并没有真的删除该结点(指cnt=0时)。那么何时删除该结点呢?当然是在unfold的时候了,若遇到一个cnt=0的结点,则不用将其存入数组,并delete它。
void __erase(node* &now,int x)
{
if(x==now->val)
now->cnt--;
else if(x<now->val)
__erase(now->lc,x);
else
__erase(now->rc,x);
update(now);
}
这时,你也许会问——若已删除结点过多,会不会影响效率?这个疑问是很自然的,毕竟这些结点占着地方,却是些空点,在插入/删除等操作中又难免要经过它们,不仅浪费时间,还浪费空间。
但是,我们不要忘了一件非常重要的事——替罪羊树是一棵平衡树。我们先前讲了那么多关于重构的知识,就是为了使替罪羊树成为一棵平衡树。
既然替罪羊树是一棵平衡树,那我们还有什么可担心的呢?树高可是log级别的啊!即使我们不去实时删除它们,也无非是多一点常数因子罢了。
如果你对这一点常数因子还是非常介意,那么这里提供两种解决思路:
(1)采用实时删除。既然问题是惰性删除带来的,那么我们不用惰性删除就好了。具体操作可以参考2.7.4。
(2)在结点中引入子树当前大小csiz(current size,每个结点记1次)和子树真实大小tsiz(true size,每个结点记1次)。若删除时发现cnt=0,则tsiz--。这样在update的时候,若发现tsiz占csiz的比例低于,则该结点亦应重构(即更新goat)。
3.2.5 替罪羊树的其他基本操作
其他基本操作与普通二叉排序树无异,下面给出完整代码实现。
#include<bits/stdc++.h>
using std::vector;
struct tree
{
private:
static constexpr double alpha=0.75;
struct node
{
int val,cnt,tot;
node *lc,*rc;
node(int _val=0,int _cnt=0,int _tot=0)
{
val=_val,cnt=_cnt,tot=_tot;
lc=rc=NULL;
}
}*root,*null,**goat;
void update(node* &now)
{
now->tot=now->cnt+now->lc->tot+now->rc->tot;
if(std::max(now->lc->tot,now->rc->tot)>alpha*now->tot)
goat=&now;
}
void __insert(node* &now,int x)
{
if(now==null)
{
now=new node(x,1,1);
now->lc=now->rc=null;
return;
}
if(x==now->val)
now->cnt++;
else if(x<now->val)
__insert(now->lc,x);
else
__insert(now->rc,x);
update(now);
}
void __erase(node* &now,int x)
{
if(x==now->val)
now->cnt--;
else if(x<now->val)
__erase(now->lc,x);
else
__erase(now->rc,x);
update(now);
}
int __rank(node *now,int x)
{
if(now==null)
return 1;
if(now->cnt&&x==now->val)
return now->lc->tot+1;
if(x<now->val)
return __rank(now->lc,x);
return now->lc->tot+now->cnt+__rank(now->rc,x);
}
int __kth(node *now,int k)
{
if(k<=now->lc->tot)
return __kth(now->lc,k);
if(k>now->lc->tot+now->cnt)
return __kth(now->rc,k-now->lc->tot-now->cnt);
return now->val;
}
void balance(node* &now)
{
vector<node*> v;
unfold(v,now);
now=rebuild(v,0,v.size()-1);
}
void unfold(vector<node*> &v,node *now)
{
if(now==null)
return;
unfold(v,now->lc);
if(now->cnt)
v.push_back(now);
unfold(v,now->rc);
if(!now->cnt)
delete now;
}
node* rebuild(vector<node*> &v,int l,int r)
{
if(l>r)
return null;
int mid=l+r>>1;
v[mid]->lc=rebuild(v,l,mid-1);
v[mid]->rc=rebuild(v,mid+1,r);
update(v[mid]);
return v[mid];
}
public:
tree()
{
root=null=new node;
}
void insert(int x)//插入
{
goat=&null;
__insert(root,x);
balance(*goat);
}
void erase(int x)//删除
{
goat=&null;
__erase(root,x);
balance(*goat);
}
int rank(int x)//查排名
{
return __rank(root,x);
}
int kth(int k)//查k大
{
return __kth(root,k);
}
int prev(int x)//求前驱
{
return kth(rank(x)-1);
}
int next(int x)//求后继
{
return kth(rank(x+1));
}
};
第3节 Treap
第4节 Splay
第5节 SBT
第6节 AVL树
第7节 红黑树
第8节 WBLT
第9节 权值线段树
1.插入一个整数x:单点+1。
2.删除一个整数x:单点-1。
3.查询整数x的排名:查询x左边的数字的个数,即区间求和。
4.查询排名为x的数:在线段树上维护size,然后每次看往左还是往右,直到单点为止。
5.求x的前驱:综合3、4操作。
6.求x的后继:综合3、4操作。
复杂度分析:(n代表操作数,R代表值域大小)
实现方式 | 离散化 | 动态开点 |
时间复杂度 | O(n*log n) | O(n*log R) |
空间复杂度 | O(n) | O(n*log R) |
是否支持在线 | 否 | 是 |
值得注意的是,由于离散化实现的常数因子较大,故在实际表现中其时间效率不如动态开点,但其空间效率总是优于动态开点。
#include<bits/stdc++.h>
#define INF 10000000
const int maxn=1e5+5;
struct tree
{
private:
int tot;
struct node
{
int l,r;
int lc,rc;
int cnt;
node(int _l=0,int _r=0)
{
l=_l,r=_r;
lc=rc=0;
cnt=0;
}
}a[maxn<<2];
void modify(int now,int pos,int k)
{
if(a[now].l==a[now].r)
{
a[now].cnt=std::max(a[now].cnt+k,0);
return;
}
int mid=a[now].l+a[now].r>>1;
if(pos<=mid)
{
if(!a[now].lc)
a[a[now].lc=++tot]=node(a[now].l,mid);
modify(a[now].lc,pos,k);
}
else
{
if(!a[now].rc)
a[a[now].rc=++tot]=node(mid+1,a[now].r);
modify(a[now].rc,pos,k);
}
a[now].cnt=a[a[now].lc].cnt+a[a[now].rc].cnt;
}
int query_cnt(int now,int l,int r)
{
if(!now||a[now].r<l||r<a[now].l)
return 0;
if(l<=a[now].l&&a[now].r<=r)
return a[now].cnt;
return query_cnt(a[now].lc,l,r)+query_cnt(a[now].rc,l,r);
}
int query_val(int now,int x)
{
if(a[now].l==a[now].r)
return a[now].l;
if(x<=a[a[now].lc].cnt)
return query_val(a[now].lc,x);
return query_val(a[now].rc,x-a[a[now].lc].cnt);
}
public:
tree()
{
a[++tot]=node(-INF,INF);
}
void insert(int x)
{
modify(1,x,1);
}
void erase(int x)
{
modify(1,x,-1);
}
int x_rank(int x)
{
return query_cnt(1,-INF,x-1)+1;
}
int rank_x(int x)
{
return query_val(1,x);
}
int prev(int x)
{
return rank_x(x_rank(x)-1);
}
int next(int x)
{
int cnt=query_cnt(1,x,x);
return rank_x(x_rank(x)+cnt);
}
};