《C/C++数据结构与算法》第三讲——平衡树

本文介绍了平衡树的概念,以避免二叉排序树在最坏情况下的低效率。重点讲解了替罪羊树的实现,包括节点结构、平衡因子、重构过程以及插入和删除操作。此外,还概述了替罪羊树的其他基本操作,如查询排名和查找特定元素。文章末尾提到了Treap作为另一种平衡二叉搜索树的引入,为后续章节埋下伏笔。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

第1节    平衡树的引入

        在上一讲中提到,由于二叉排序树的复杂度很容易退化,因此在实际中的用途没有那么广。但是如果二叉排序树能实现平衡,那么二叉排序树的时间复杂度为O(log_2n),非常优秀,可以得到非常广泛的应用。

        这里所谓的“平衡”,在不同的地方有着不同的定义, 常见的有高度平衡树、重量平衡树等。但其主要目标,都是保证结点最大深度不超过O(log_2n),以实现高效的查找效率。

        在接下来的几节中,我们将通过下面的两道例题,来依次介绍替罪羊树、Treap、Splay等一些常用的平衡方法。

         【例3.1.1】普通平衡树

        您需要写一种数据结构(可参考题目标题),来维护一个数集,其中需要提供以下操作:

        1.插入一个整数x。

        2.删除一个整数x(若有多个相同的数,只删除一个)。

        3.查询整数x的排名(排名定义为比当前数小的数的个数+1)。

        4.查询排名为x的数。

        5.求x的前驱(前驱定义为小于x且最大的数)。

        6.求x的后继(后继定义为大于x且最小的数)。

        保证所有操作合法。

        【例3.1.2​​​​​​】普通平衡树(数据加强版)

        在上一题的基础上,扩大数据范围并增加了强制在线

第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  替罪羊树的重构

        首先,我们需要判定一个结点是否应重构。为此,我们引入一个平衡因子\alpha​(取值在(0.5,1),一般采用0.7或0.8),若某结点的子结点大小占它本身大小的比例超过\alpha​,则该结点是不平衡的,应当重构。

        另外,为了避免一些不必要的重构(例如某结点的儿子是不平衡的,重构完又发现该结点还是不平衡的,于是再次重构,而事实上只需重构该结点即可),我们引入替罪羊结点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的比例低于\alpha​,则该结点亦应重构(即更新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);
	}
};
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值