平衡树算法总结&专题训练2(有旋平衡树:AVL 树,Splay)

本文详细介绍了两种有旋平衡树:AVL树和Splay树。AVL树是最早的平衡树,通过树旋转保持平衡,效率高但操作有限。Splay树则通过伸展操作使常用节点靠近根部,具有很好的自适应性。AVL树适用于追求高效稳定的操作,Splay树则在动态查询中表现出色。两者各有优缺点,AVL树时间复杂度优秀,Splay树可扩展性强。
摘要由CSDN通过智能技术生成

一些 update

udpate 2021/1/17:修正了文章中的一些不影响理解的语文问题。

update 2021/1/25:加入 Splay 的代码简化。

1.概述

平衡树算法总结&专题训练1 中我们着重讲解了两种无旋平衡树:替罪羊树,FHQ Treap。

那么在这篇博文中我们将讲解 AVL 树和 Splay 这两种有旋平衡树。

有旋平衡树维持树平衡的方法通常都是树旋转。

我专门写了一篇文章来讲树旋转->link,请务必保证在学透树旋转之后再往下看,否则会看不懂的。

那么还是以模板为例,看看 AVL 树和 Splay 又会有怎样的表现。

P3369 【模板】普通平衡树

2.有旋平衡树-AVL 树

实际上,AVL 树非常久远,或许很多人在学平衡树之前听说过“平衡树双子星”(FHQ Treap,Splay),也可能听过替罪羊树,但是 AVL 树很少有人听到。

其实 AVL 树是最早被发明的平衡树,但是随着时代的推进,AVL 树逐渐被拍在沙滩上,现在已经不多见。但是论效率,AVL 树是效率最高的一种树。

当然 AVL 树不学也没多大问题,毕竟很多人都没有学过 AVL 树。

1. 思路

AVL 树维持平衡的一个重要思路就是利用树旋转,树旋转的操作可以使得一条链状二叉查找树变得相对平衡。

但是我们也不能随便树旋转啊,那么我们到底什么时候要树旋转呢?

这里就要引入 AVL 树中一个重要的概念:平衡因子(BF)

平衡因子的计算公式:BF = 左子树高度 - 右子树高度。(当然也可以反过来)

那么知道 BF 又有什么用呢?

我们知道,BST 中最坏的情况就是找到深度最深的点,而如果我们能够将每个节点的 BF 控制在 -1,0,1 之内,那么这棵树的高度肯定是趋于 log ⁡ n \log n logn 的。

而如果一个节点不平衡了,那么我们需要调整。

2. AVL 树的平衡判断-check

AVL 树的平衡判断有 4 种类型:LL,LR,RL,RR。

这四种类型的解释如下:

  • LL:表示当前节点的 子树太高了,而 子树的 子树比较高。
  • LR:表示当前节点的 子树太高了,而 子树的 子树比较高。
  • RL:表示当前节点的 子树太高了,而 子树的 子树比较高。
  • RR:表示当前节点的 子树太高了,而 子树的 子树比较高。

那么太高和比较高的定义是什么?

  • 太高:当前节点的 B F < − 1 BF < -1 BF<1 或者 B F > 1 BF > 1 BF>1
  • 比较高:当前节点的 B F < 0 BF < 0 BF<0 或者 B F > 0 BF > 0 BF>0

而针对这四种类型,我们分别有两大类,四小类方法解决。

2.1 LL 型 & RR 型

针对 LL 型和 RR 型,操作相对简单。

  • LL型:我们对不平衡节点做一次右旋,上图:

在这里插入图片描述

  • RR 型:我们对不平衡节点做一次左旋,上图:

在这里插入图片描述

2.2 LR 型 & RL 型

这两种情况稍微有一点麻烦。

  • LR型:先左旋自己的左儿子(此时变成了 LL 型),再右旋自己。上图:

在这里插入图片描述

  • RL 型:先右旋自己的右儿子(此时变成了 RR 型),再左旋自己。上图:

在这里插入图片描述

2.3 代码

代码:

void check(int &now)//注意引用
{
	int nf = BF(now);//BF 是获取平衡因子
	if (nf > 1)//左子树太高
	{
		int lf = BF(tree[now].l);
		if (lf > 0) rrotate(now);//左子树比较高
		else lrotate(tree[now].l), rrotate(now);//右子树比较高
	}
	else if (nf < -1)//右子树太高
	{
		int rf = BF(tree[now].r);
		if (rf < 0) lrotate(now);//右子树比较高
		else rrotate(tree[now].r), lrotate(now);//左子树比较高
	}
	else if(now) update(now);//比较平衡,直接更新
}

3. 一些基础函数

3.1 结构体

我们要存这样 5 个值:左儿子,右儿子,值,树高,树的大小。

为什么没有平衡因子?因为我们可以现场计算平衡因子。代码不给了。

3.2 Make_Node & update & BF

代码:

void Make_Node(int &now, int val)
{
	tree[now = ++cnt].val = val;
	tree[cnt].size = 1;
}

int BF(int x) {return tree[tree[x].l].hei - tree[tree[x].r].hei;}
void update(int x)
{
	tree[x].size = tree[tree[x].l].size + tree[tree[x].r].size + 1;
	tree[x].hei = max(tree[tree[x].l].hei, tree[tree[x].r].hei) + 1;
	//注意更新高度 
}

这里说明一下:初始化的时候指定初始的高度为 0 或者 1 都没有问题,因为我们最后是按照 BF 来算的,而这样初始化对 BF 不会有任何影响。

3.3 lrotate & rrotate

代码:

void lrotate(int &now)//在之前的树旋转博客中有讲解,这里就不说了
{
	int r = tree[now].r;
	tree[now].r = tree[r].l;
	tree[r].l = now;
	now = r;
	update(tree[now].l), update(now);
}

void rrotate(int &now)//在之前的树旋转博客中有讲解,这里就不说了
{
	int l = tree[now].l;
	tree[now].l = tree[l].r;
	tree[l].r = now;
	now = l;
	update(tree[now].r), update(now);
}

4. 插入数据-Insert

插入:按照二叉查找树的方式插入,最后不要忘记自底向上检查树是否平衡。

代码:

void Insert(int &now, int val)//注意引用
{
	if (!now) Make_Node(now, val);
	else if (tree[now].val > val) Insert(tree[now].l, val);
	else Insert(tree[now].r, val);
	check(now);//自底向上检查
}

5. 删除数据-Delete

5.1 思想

AVL 树的删除数据跟之前有一点不一样。

替罪羊树:惰性删除。

FHQ Treap:分裂再合并。

AVL 树:但是这些我都做不到啊······我好像除了是棵会旋转的二叉查找树也不会别的了?

那么我们就按二叉查找树来删!

我们首先先找到要删除这个节点的后继(需要注意的是,这跟『找后继-Find_aft』没有任何联系),然后首先将这个节点『伪删除』,同时更新父节点数据。然后将『伪删除』的节点覆盖我们真正需要删除的节点,然后再次更新数据。

那么为什么说这跟『找后继-Find_aft』没有关系呢?这是因为找后继只是单纯的找后继,但是因为这里会将后继移走,所以我们还需要自底向上判断树是否平衡。

5.2 Delete 的找后继-Find

从需要的节点的右子树开始,不断的往左找即可。

代码:

int Find(int &now, int fa)//还是注意引用
{
	int sum;
	if (!tree[now].l)//没有左子树了
	{
		sum = now;//找到答案
		tree[fa].l = tree[now].r;//替换数据
	}
	else
	{
		sum = Find(tree[now].l, now);//继续找
		check(now);//检查平衡与更新数据
	}
	return sum;
}

5.3 Delete

我们先找到这个节点,然后用后继替代他即可。

代码:

void Delete(int &now, int val)//注意引用
{
	if (val == tree[now].val)//找到想要的节点
	{
		int l = tree[now].l, r = tree[now].r;//先暂存数据
		if (!l || !r) now = l + r;//叶子节点直接删除
		else
		{
			now = Find(r, r);//找到后继 
			if (now != r) tree[now].r = r;//更新数据
			tree[now].l = l;//更新数据
		}
	}
	else if (tree[now].val > val) Delete(tree[now].l, val);
	else Delete(tree[now].r, val);
	check(now);//自底向上检查
}

6. 其他函数

6.1 找排名(Find_Rank) & 找第 k 大(Find_kth)

替罪羊树怎么搞,我们就怎么搞。

代码:

int Find_Rank(int val)
{
	int now = root, ans = 1;
	while (now)
	{
		if (val <= tree[now].val) now = tree[now].l;
		else {ans += tree[tree[now].l].size + 1; now = tree[now].r;}
	}
	return ans;
}

int Find_kth(int val)
{
	int now = root;
	while (now)
	{
		if (tree[tree[now].l].size + 1 == val) break;
		if (tree[tree[now].l].size >= val) now = tree[now].l;
		else {val -= tree[tree[now].l].size + 1; now = tree[now].r;}
	}
	return tree[now].val;
}

6.2 找前驱(Find_pre) & 找后继(Find_aft)

可以自己打,也可以利用前面两个函数。作者因为懒所以用的是前面两个函数。

式子:

Find_pre(x) = Find_kth(Find_Rank(x) - 1);
Find_aft(x) = Find_kth(Find_Rank(x + 1));

7. 最后的代码

限于文章篇幅问题,完整代码请在 这里 查看。

8. AVL 树的好处

常数很小!!!!!!这是我目前为止学到的所有平衡树中最快的平衡树,时间吊打其他 3 棵平衡树,而且空间也相当优秀,具体的对比看文章末尾。

9. AVL 树的坏处

由于其能够支持的操作相对较少,因此现在正在逐渐消失于 OI 中,只有少数地方能看到他。其实不学 AVL 树都没事。

3. 有旋平衡树-Splay

1. 思路

Splay,中文名为伸展树,是一种强而有力的数据结构,可扩展性很强,有“平衡树双子星”之称(另一棵是 FHQ Treap),而 Splay 维护平衡也是基于树旋转操作的。

2. 结构体建立 & 基础函数

特别提醒:Splay 的结构体建立与基础函数和之前有一点小小的差别,但是如果不注意这点差别 Splay 就很容易写错。

我们需要存这 5 个值:左儿子 l l l ,右儿子 r r r ,子树大小 s i z e size size,值 v a l val val和当前值重复次数 c n t cnt cnt

需要注意这个 c n t cnt cnt ,因为如果我们不采用 c n t cnt cnt 将会特别难写(至于有多难写各位读者可以自己尝试一下)。

网络上有很多的文章还维护了一个 f a fa fa 来存当前节点的父亲,但是我认为这样没有必要——这样对于初学者很不友善,而且不存 f a fa fa 效率也没有差多少。

然后就是基础函数 update,Make_Node,zig,zag ⁡ \operatorname{update,Make\_Node,zig,zag} update,Make_Node,zig,zag,其中 zig,zag ⁡ \operatorname{zig,zag} zig,zag 对应右旋和左旋(就是换了个名字)。

贴一下代码:

struct node
{
	int l, r, size, cnt, val;
}tree[MAXN];

void update(int x) {tree[x].size = tree[tree[x].l].size + tree[tree[x].r].size + tree[x].cnt;}
//																注意这里不是 1!是 tree[x].cnt!

void Make_Node(int &now, int &val)
{
	tree[now = ++cnt].val = val;
	tree[cnt].size++; tree[cnt].cnt++;
}

void zig(int &now)
{
	int l = tree[now].l;
	tree[now].l = tree[l].r;
	tree[l].r = now;
	now = l;
	update(tree[now].r), update(now);
}

void zag(int &now)
{
	int r = tree[now].r;
	tree[now].r = tree[r].l;
	tree[r].l = now;
	now = r;
	update(tree[now].l), update(now);
}

3. Splay 的核心操作-Splay

3.1 概述

Splay 的核心操作是伸展(Splay)操作。

知道我们的输入法吗?我们会发现,平常有的字我们打的多了,这些字就会自动出现在最前面,而这也是 Splay 的思想。

Splay 的思想就是:将所有用到的节点(不管是插入数,找排名,找前驱,找后继等等)全部伸展到根节点,通过这样来维护树平衡。

那么怎样旋转呢?最朴素的思路就是将当前节点直接依次旋转上去(称为『单旋』),但是这样会有问题。

比如我们有下面这样一棵树(假设我们刚刚插入了 1):

在这里插入图片描述

需要注意的是,如果只使用单旋操作,这种情况是存在的,可以构造数据卡掉。

那么我们要把 1 单旋到根节点,那么先右旋 2,再右旋 3,······,最后右旋 6。

然后手动模拟一遍就变成了下面这个样子:

在这里插入图片描述

?!这不还是一条链吗?

所以仅靠单旋是不可以的,我们需要引入『双旋』操作。

双旋操作也分 4 种类型:LL,LR,RL,RR。

设当前节点为 x x x f f f x x x 的父亲, g g g f f f 的父亲, r s o n ( x , f ) rson(x,f) rson(x,f) 判断 x x x 是否为 f f f 的右儿子,值为 1 1 1 就是,值为 0 0 0 就不是。

  • LL: r s o n ( x , f ) = 0 rson(x,f)=0 rson(x,f)=0, r s o n ( f , g ) = 0 rson(f,g)=0 rson(f,g)=0
  • LR: r s o n ( x , f ) = 0 rson(x,f)=0 rson(x,f)=0, r s o n ( f , g ) = 1 rson(f,g)=1 rson(f,g)=1
  • RL: r s o n ( x , f ) = 1 rson(x,f)=1 rson(x,f)=1, r s o n ( f , g ) = 0 rson(f,g)=0 rson(f,g)=0
  • RR: r s o n ( x , f ) = 1 rson(x,f)=1 rson(x,f)=1, r s o n ( f , g ) = 1 rson(f,g)=1 rson(f,g)=1

那么我们仍然分两大类,四小类来讨论。

接下来的讨论使用 Zig,Zag ⁡ \operatorname{Zig,Zag} Zig,Zag 表示右旋、左旋。

3.2 LL 型 & RR 型

首先我们看看 LL 型和 RR 型要怎么做。

  • LL 型:

g g g 作两次右旋即可,上图:

在这里插入图片描述

需要提示的是:我们说的对 g g g 右旋两次不是真正的对 g g g 右旋两次,而是因为在第一次旋转过后指向 g g g 的指针已经指向了 f f f ,所以说对 g g g 右旋两次,实质是对最上面的节点右旋两次。

  • RR 型:对 g g g 左旋两次,上图:

在这里插入图片描述

没错其实就是我把 LL 型的图拿来换了一下字母而已

还是注意对 g g g 左旋两次不是真正的对 g g g 左旋两次,而是对最上面的节点左旋两次。

这里二次说明:由于 Zig,Zag ⁡ \operatorname{Zig,Zag} Zig,Zag 中我们使用了引用,因此在引用 Zig,Zag ⁡ \operatorname{Zig,Zag} Zig,Zag 之后我们传进去的 g g g 和传出来的 g g g 不是一个 g g g 了。

3.3 LR 型 & RL 型

  • LR 型:先左旋自己的左儿子,然后右旋自己,上图(蓝色箭头指的是指针,不写了):

在这里插入图片描述

  • RL 型:先右旋自己的右儿子,再左旋自己,上图:

在这里插入图片描述

3.4 Splay 操作的正确性验证

有的读者会问了:那么为什么这样就是正确的呢?我感觉上面的那条链还是一条链啊。

那么我们手动模拟一遍,上图:

在这里插入图片描述

那么首先是 LL 型,转一次后变成了这样:

在这里插入图片描述

然后还是 LL 型,再转一次后变成了这样:

在这里插入图片描述

最后我们发现只有一步之遥了,那么直接单旋即可,最后是这样:

在这里插入图片描述

于是树的高度减小了 1,有用!

那么为什么上面的 LL 型和 RR 型看起来没有用呢?

实际上如果你把 x , f , g x,f,g x,f,g 的左右儿子全部添加上去,那么就会发现很有用,高度大大减小,而正是因为这一特性,所以 Splay 操作能够让 Splay 保持平衡。

3.5 代码

我们在写代码时要注意两点:

  1. 不能将 Zig,Zag ⁡ \operatorname{Zig,Zag} Zig,Zag 弄反。
  2. 如果只有一步之遥了要采用单旋。

代码:

void Splay(int x, int &y)//将 x 伸展到 y 这个节点
{
	if (x == y) return ;//到了,不用伸展了
	int &l = tree[y].l, &r = tree[y].r;
	if (l == x) zig(y);
	else if (r == x) zag(y);//以上两种为只有一步之遥,使用单旋
	else
	{
		if (tree[x].val < tree[y].val)//L
		{
			if (tree[x].val < tree[l].val) Splay(x, tree[l].l), zig(y), zig(y);//LL
			else Splay(x, tree[l].r), zag(l), zig(y);//LR
		}
		else//R
		{
			if (tree[x].val > tree[r].val) Splay(x, tree[r].r), zag(y), zag(y);//RR
			else Splay(x, tree[r].l), zig(r), zag(y);//RL
		}
	}//双旋,注意不能搞错变量,采用自底向上递归式旋转
}

4. 删除数据-Delete

4.1 思路

首先为了方便,防止维护父亲节点的值(实际上我们也没存父亲节点),因此我们首先要找到这个节点并且 Splay 到根,然后使用它的后继代替它,更新节点数据然后换掉根即可。

不过:

  1. 如果当前节点的 c n t > 1 cnt>1 cnt>1 直接让 c n t − 1 cnt - 1 cnt1 即可,不用自底向上更新数据(根节点还有父亲?)
  2. 如果当前节点没有后继,直接让左儿子代替(如果左儿子没有会默认为 0)。

4.2 代码

代码:

void del(int now)//注意没有引用
{
	Splay(now, root);//先转到根节点
	if (tree[now].cnt > 1) {tree[now].size--; tree[now].cnt--; return ;}//重复次数 - 1
	if (tree[root].r)//有后继 
	{
		int o = tree[root].r;
		while (tree[o].l) o = tree[o].l;//找到后继
		Splay(o, tree[root].r);//Splay 到右儿子
		tree[tree[root].r].l = tree[root].l;
		root = tree[root].r;//替换
		update(root);//更新数据
	}
	else root = tree[root].l;//没有后继
}

void Delete(int now, int &val)
{
	if (tree[now].val == val) del(now);//找到就删除
	else if (tree[now].val > val) Delete(tree[now].l, val);
	else Delete(tree[now].r, val);//继续找
}

5. 其他函数

5.1 Insert

注意特判当前插入的数是否已经出现过。

代码:

void Insert(int &now, int &val)
{
	if (!now) Make_Node(now, val), Splay(now, root);//注意要 Splay 到根!!!!!!
	else if (tree[now].val > val) Insert(tree[now].l, val);
	else if (tree[now].val < val) Insert(tree[now].r, val);
	else tree[now].size++, tree[now].cnt++, Splay(now, root);//相同数值特判
}

5.2 Find_Rank & Find_kth & Find_pre & Find_aft

跟之前几乎没有差别,唯一注意 c n t cnt cnt 就好。

特别提醒:在 Find_Rank 和 Find_kth 时一旦找到节点一定要 Splay 到根!!!!!!

代码:

int Find_Rank(int val)
{
	int now = root, ans = 1;
	while (now)
	{
		if (tree[now].val == val)
		{
			ans += tree[tree[now].l].size;
			Splay(now, root); break;
		}
		if (val <= tree[now].val) now = tree[now].l;
		else {ans += tree[tree[now].l].size + tree[now].cnt; now = tree[now].r;}
	}
	return ans;
}

int Find_kth(int val)
{
	int now = root;
	while (now)
	{
		if (tree[tree[now].l].size < val && tree[tree[now].l].size + tree[now].cnt >= val) {Splay(now, root); break;}
		else if (tree[tree[now].l].size >= val) now = tree[now].l;
		else {val -= tree[tree[now].l].size + tree[now].cnt; now = tree[now].r;}
	}
	return tree[now].val;
}

Find_pre 和 Find_aft?直接公式计算。

6.特别提醒!!!

一定要注意:Insert & Find_Rank & Find_kth 这三个函数在完成操作之后一定要将被操作的节点 Splay 到根节点!!!!!!不然就会因为数的高度不平衡而导致 TLE!!!!!!

7. 最后的代码

代码请在 这里 查看。

8. Splay 代码简化

Splay 的代码实在是太长了qwq,而且 Z i g , Z a g Zig,Zag Zig,Zag 很容易搞混,那么我们有没有什么办法能够减少它的码量呢?

有!

8.1 结构体的更新

在 Splay 的代码简化当中,我们将采用 c h [ 0 / 1 ] ch[0/1] ch[0/1] 来代替左右儿子,其中 0 表示左儿子,1 表示右儿子,同时还要维护父亲节点。代码不贴了。

8.2 判断儿子关系-rson

这个也很简单,判断一个节点的右儿子是否为另一个节点,一行就可以搞定。代码不给了,可以在最后查看。

8.3 父子关系建立-connect

我们首先需要一个 connect 函数帮助我们来建立两个节点的父子关系。

代码:

void connect(int x, int y, int f)
{
	tree[y].ch[f] = x;
	tree[x].fa = y;
}//表示将 x 变为 y 的儿子,左右标记为 f

8.4 Zig-Zag 合并-rotate

Splay 代码简化的一个核心就是将 Zig-Zag 合并称为一个函数 rotate。

那么我们重新审视树旋转的口诀:“左旋拎右左挂右,右旋拎左右挂左”。

注意:接下来对 x x x 节点旋转指旋转的是 x x x 的父节点!

那么假如我们要旋转一个节点,那么我们首先要左挂右/右挂左。

这两个的特点是:被挂的节点与旋转的节点左右儿子关系都与旋转的节点和父节点不一样。

说的通俗点,从父亲节点到被挂的节点构成一个 ‘<’/’>’ 的形状。

那么我们可以用 rson 函数合并。

设旋转节点 x x x,父亲节点 f f f,祖父节点 g f gf gf k = r s o n ( x , f ) k = rson(x,f) k=rson(x,f)

那么 t r e e [ x ] . c h [ k ⊕ 1 ] tree[x].ch[k \oplus 1] tree[x].ch[k1] 就是要被挂的节点。

这里异或操作可以进行 0->1,1->0 的操作。

那么我们使用 connect 操作连接被挂节点和 f f f,儿子关系为 k k k

接下来我们再用两次 connect,一次连接 x x x g f gf gf,儿子关系为 r s o n ( f , g f ) rson(f,gf) rson(f,gf),一次连接 f f f x x x,儿子关系为 k ⊕ 1 k \oplus 1 k1

想不通的读者可以回到树旋转的博文结合图片去思考。

那么三次 connect 就可以完成操作。

代码:

void rotate(int x)
{
	int f = tree[x].fa, gf = tree[f].fa, k = rson(x, f);
	connect(tree[x].ch[k ^ 1], f, k);
	connect(x, gf, rson(f, gf));
	connect(f, x, k ^ 1);//三次 connect 解决
	update(f), update(x);
}

8.5 伸展操作-Splay

现在我们都将 Zig-Zag 合二为一了我们难道还需要那么麻烦吗qwq。

在代码简化版中,Splay(x,target) 的定义是: x x x 伸展到 t a r g e t target target 的下面。

我们重新审视 Splay 的四种情况:

  • LL/RR:我将其称为同偏型。
  • LR/RL:我将其称为异偏型。

对于同偏型:都是先转父亲节点再转自己。

对于异偏型:都是转两次自己。

那么我们不是又可以利用 rson 操作解决了?

代码:

void Splay(int now, int target)
{
	if (!target) root = now;//注意 target = 0 时要更新根节点数据
	while (tree[now].fa != target)//判断有没有到
	{
		int f = tree[now].fa, gf = tree[f].fa;
		if (gf == target) {rotate(now); return ;}//祖父节点就是 target,单旋
		if (rson(now, f) == rson(f, gf)) rotate(f);//同偏型
		else rotate(now);//异偏型
		rotate(now);//注意最后都是旋转自己
	}
}

8.6 其他函数

其实没什么好说的,唯一需要注意的是 Make_Node 和 Delete 函数记得维护父亲节点。

8.7 最后的代码

代码请在 这里 查看。

9. Splay 的好处

Splay 的好处很多!比如能搞很多 替罪羊树/AVL 树 搞不了的东西,还有 Splay 操作能够用于很多场合(比如 LCT),还有很多很多······

10. Splay 的坏处

当然 Splay 还是有一定的坏处的,比如常数较大(见最后的表格对比)。

4. 四种平衡树的对比

本人只是按照自己码的代码来对比四种平衡树,如果有不妥之处请谅解,如果有误请指出。

先上表格(均以洛谷为准,不考虑评测机波动,鉴于某些原因 Splay 代码简化版只给出码量供参考):

种类平衡树代码链接时间/空间代码行数/代码长度
无旋平衡树替罪羊树link329ms/1.87MB157/3.47KB
无旋平衡树FHQ Treaplink291ms/1.48MB140/2.48KB
有旋平衡树AVL 树link250ms/1.50MB151/2.93KB
有旋平衡树Splaylink363ms/2.63MB143/3.18KB
有旋平衡树Splay 代码简化link/132/2.82KB

友情提醒:Splay 代码简化的数据只是估计数据,预计行数误差不超过 3 行,码量误差不超过 0.03KB。

各位读者或许能够从上面看到些什么。

我个人根据上面的表格做出了一些总结:

  • 从时间上看:AVL 树最优,FHQ Treap 表现良好,替罪羊树和 Splay 比较劣。
  • 从空间上看:FHQ Treap 和 AVL 树最优,替罪羊树表现良好,Splay 比较劣。
  • 从代码长度上看:FHQ Treap 最短,AVL 树和 Splay 表现良好(当然代码简化版好一点),替罪羊树比较劣。
  • 从可扩展性来看:FHQ Treap 和 Splay 最优,替罪羊树表现良好,AVL 树比较劣(虽然这栏表格里没有)。
  • 从种类内部来看:无旋平衡树中 FHQ Treap 各方面都比替罪羊树好,但是替罪羊树易于理解而且容易上手;有旋平衡树中 AVL 树的时间、空间、代码长度都比 Splay 好,但是 Splay 的可扩展性比 AVL 树要好的多。而且 Splay 代码简化版代码长度比 AVL 树要短!
  • 从整体上来看:“平衡树双子星” FHQ Treap 和 Splay 要优于 替罪羊树 和 AVL 树。

这只是我个人的一个对比和总结,各位读者不喜勿喷。

5. 总结

这里再放一下 4 种平衡树的主要思路吧!

排名不分先后。

  1. 替罪羊树:如果一棵子树的左子树或右子树所占节点数超过总节点个数 × a l p h a \times alpha ×alpha(一般取 0.75)或者懒删除节点超过总节点个数的 30 % 30\% 30% 那么就拍扁重构。
  2. FHQ Treap:使用分裂(Split) 和 合并(merge)来实现各种操作。
  3. AVL 树:使用平衡因子 BF 控制高度平衡。
  4. Splay:使用伸展操作(Splay)来控制整棵树相对平衡,使用次数越多的节点越靠前。

考虑到 OI 中的实用性以及其他原因,接下来的代码将会采用 FHQ Treap 和 Splay 来写。替罪羊树和 AVL 树可能会出现的很少。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值