今天要来点自平衡的二叉搜索树吗——平衡树学习笔记

平衡树学习笔记

在学习平衡树之前,我们首先得弄懂什么是平衡树……——引言

提示:本篇约3000字,学习此内容可能需要2小时

复习部分:

先回忆一下二叉搜索树,当插入一个元素时,如果它比父节点大,就作为父节点的左儿子,否则作为父节点的右儿子。

下面这一坨就是一棵二叉搜索树

如图,当我们需要在搜索树中搜索某个元素时,只需要看它与左右儿子的大小关系,然后向左或者向右走就可以了。

二叉搜索树本来是一种很优秀的算法,搜索每一个元素的期望步数是(logN)的,因此它的时间复杂度是(O(logN))

但是,二叉搜索树如果遇到构造数据,将会退化。如图,如果 毒瘤 出题人按顺序给出一段序列,你的二叉搜索树就会变成这个鬼样子:

???说好的二叉搜索树呢?这TM是一条链啊!

就这样,你的二叉搜索树变成了暴力结构(查询的时间为(O(N))),还不如直接写数组爆搜。这显然是我们很讨厌的,因此,我们需要想一个办法 给出题人一点教训 优化一下二叉搜索树,让其实现自平衡。

基于这个想法,我们制造了一种数据结构——

平衡树

[ ext{———————————————萌萌的分割线———————————————} ]


我们发现,二叉搜索树退化,归根到底是其一条链太长,自身不平衡所导致的时间复杂度退化。

因此,我们需要想一个办法让它平衡平衡……

设想一下,如果当某一条链太长,我们就把中间的点给拎起来,它不就平衡多了吗?

让我们模拟一下:

woc好像有点道理啊,那该怎么旋转呢?


旋转方式

通过分析,我们发现旋转有两种方式:

左旋,以及右旋

不懂没有关系,我们看一下下面的例子

这里我们将(X)节点右旋到了(Y)节点,发生了以下的一些事情:

  • (X)的左儿子没有变化

  • (Y)变为了(X)的右儿子

  • (Y)的右儿子没有变化

  • (X)的右儿子变为了(Y)的左儿子

emmmmmmmm……这是右旋操作,左旋操作同理,各位可以自己模拟一下。

好了,我们已经知道旋转的规则了,那我们来做一道题吧:


您需要写一种数据结构(可参考题目标题),来维护一些数,其中需要提供以下操作:
1. 插入xx数
2. 删除xx数(若有多个相同的数,因只删除一个)
3. 查询xx数的排名(定义为比当前数小的数的个数+1。若有多个相同的数,输出最小的排名)
4. 查询排名为xx的数
5. 求xx的前驱(前驱定义为小于xx,且最大的数)
6. 求xx的后继(后继定义为大于xx,且最小的数)

这就是平衡树的模板题了,怎么样,是不是很简单?(划去)

[small ext{(仅吐槽)} ]

我们考虑一下怎么实现一棵平衡树

首先开一个结构体储存信息,包含节点的数值,父亲节点是谁,左右儿子,它儿子的数量以及这个元素的个数(重复次数)

#define lson ch[0]
#define rson ch[1]

struct Node
{
	int v,fa,ch[2],num_son,cnt;
	//节点值,父节点,左右儿子,儿子数量,重复次数
}t[100005<<3];

使树平衡的方法是对一个节点进行左旋/右旋(下图为右旋)

之前我们已经讲过下面这段话:

我们将(X)节点右旋到了(Y)节点,发生了以下的一些事情:

  • (X)的左儿子没有变化

  • (Y)变为了(X)的右儿子

  • (Y)的右儿子没有变化

  • (X)的右儿子变为了(Y)的左儿子

因为左旋和右旋的旋转方式是相反的,因此我们可以考虑只写一个函数,自动选择实现哪一种操作

自然地,写一个(identify)函数来确定一个节点与其父节点的关系,这样就可以做到自动判断该左旋还是右旋

int Identify(int x)//确定与父节点的关系
{
	return t[t[x].fa].lson==x?0:1;
	//其父亲的左儿子是不是自己,可以判断自己是不是左儿子
}

判断完左旋右旋后,该处理节点了。大家应该还记得一个位运算的技巧吧?

101^001=100(5^1=4)

1100^0001=1101(8^1=9)

……

多推几个式子,可以发现——奇数xor1的值为它减一,偶数xor1的值为它加一

因此,对于任何一个节点,将它的值异或1就可以求出它父亲的另一个子节点的编号(举个例子,它是右儿子的话,我们只需要将它异或1就可以求出左儿子的编号)。配合我们写好的自动判断左右儿子的函数,我们就可以做到自动判断旋转方式(左旋/右旋),自动操作。

下面是旋转的代码(无法理解的话可以先将k看为1,即右旋操作来理解,左旋完全相反)

void Rotate(int x)
{
	int y=t[x].fa;//y是x原来的父亲
	int z=t[y].fa;//z是x原来的爷爷
	int k=Identify(x);//自己是左还是右儿子(下面的注释以右旋,即k=0为例)

	t[z].ch[Identify(y)]=x;//这个节点原来的爷爷的儿子是自己
	t[x].fa=z;//将这个节点的父亲设为原来自己的爷爷
	//上面两步即取代父亲的位置

	t[y].ch[k]=t[x].ch[k^1];//它的右儿子变为父亲的左儿子
	t[t[x].ch[k^1]].fa=y;//它左儿子的父亲是它右儿子的父亲,即统一父亲

	t[x].ch[k^1]=y;//它的右儿子是自己的父亲
	t[y].fa=x;//它原来的父亲变为了儿子

	Update(y);
	Update(x);//每次操作后需要维护
}
(这一段比较复杂,建议将上面的动图与注释结合在一起看。其实这段函数的本质就是实现了一次旋转,使用位运算达到了自动判断左右旋,然后进行相反的操作罢了)

需要注意的是,每次操作后,都需要维护节点儿子数量的信息,否则会出错,也就是上面函数里的Update

这个非常好实现,相加维护一下就好了

void Update(int x)//维护儿子数量
{
	t[x].num_son=t[t[x].lson].num_son+t[t[x].rson].num_son+t[x].cnt;
}

接下来,是伸展(Splay)操作,即一些博客中所说的“上旋”操作

上旋也有两种方式:一种是右旋两次(下图上部分);另一种是先左旋一次再右旋一次(下图下部分)

判断一下,然后右旋就可以了

void Splay(int x,int goal)//把x节点旋转到某个位置
{
	while(t[x].fa!=goal)//如果还没达到目的
	{
		int y=t[x].fa;
		int z=t[y].fa;
		if(z!=goal)//如果它目标不是它的爷爷节点
			(t[y].lson==x)^(t[z].lson==y)?Rotate(x):Rotate(y);
		Rotate(x);
	}
	if(!goal) root=x;
}

此外,我们还需要完成一些题目所需要的操作。

对于插入一个节点,我们需要判断它应该被插入到哪里,以及这个数字是否存在过。如果存在过,我们只需要将这个数字的计数器+1;而如果是一个新的元素,就需要更新这个节点的诸多信息,最后Splay一下。

void Insert(int x)//插入一个节点x
{
	int u=root,fa=0;
	while(u&&t[u].v!=x)
	{
		fa=u;
		u=t[u].ch[x>t[u].v];
	}
	if(u) ++t[u].cnt;//如果已经有这个数字了,计数+1
	else//如果不存在这个数字,就加入新的节点
	{
		u=++tot;
		if(fa) t[fa].ch[x>t[fa].v]=u;
		t[tot].lson=0;
		t[tot].rson=0;
		t[tot].fa=fa;
		t[tot].v=x;
		t[tot].cnt=1;
		t[tot].num_son=1;
	}
	Splay(u,0);
}

查询排名,前驱后继以及删除操作就比较简单,大家可以合着代码理解一下

void Find(int x)//查询x的排名
{
	int u=root;
	if(!u) return;//不存在节点,无法查找排名
	while(t[u].ch[x>t[u].v]&&x!=t[u].v)
		u=t[u].ch[x>t[u].v];
	Splay(u,0);
}

int Next(int x,int opt)//0为前驱,1为后继
{
	Find(x);
	int u=root;
	if((t[u].v>x&&opt)||(t[u].v<x&&!opt)) return u;
	u=t[u].ch[opt];
	while(t[u].ch[opt^1]) u=t[u].ch[opt^1];
	return u;
}

void Delete(int x)
{
	int last=Next(x,0);//x的前驱
	int next=Next(x,1);//x的后继
	Splay(last,0);Splay(next,last);
	int del=t[next].lson;
	if(t[del].cnt>1)
	{
		--t[del].cnt;//存在多个的数字被删去,计数-1
		Splay(del,0);
	}
	else t[next].lson=0;//清除节点
}

int Kth(int x)//查询第k名是哪个数
{
	int u=root;
	if(t[u].num_son<x) return false;//没有这么多数
	while(1)
	{
		int y=t[u].lson;
		if(x>t[y].num_son+t[u].cnt)//在排名为u的数的后面
		{
			x-=t[y].num_son+t[u].cnt;//减去那么多
			u=t[u].rson;//在右子树中找
		}
		else if(t[y].num_son>=x) u=y;//如果y的节点多于x,在左子树中找
		else return t[u].v;//找到了,返回结果
	}
}

至此,一棵平衡树便实现了。

我们来总结一下一棵平衡树必要的函数(排开题目所需操作)

  • Update,更新操作。每一次操作完成后都需要更新节点的儿子数量,使其平衡

  • Identify,鉴定操作。根据一个节点父亲和它自己的关系,确定它是左儿子还是右儿子,从而确定旋转的方式

  • Rotate,旋转操作。通过节点的左旋(右旋),达到让树平衡,防止其时间复杂度退化为(O(N))

  • Splay,伸展操作。即上旋,实现两次左右旋转

最后,放出上一道题完整代码:

#include<bits/stdc++.h>
#define lson ch[0]
#define rson ch[1]
using namespace std;

int T,opt,x;
int root,tot;

struct Node
{
	int v,fa,ch[2],num_son,cnt;
	//节点值,父节点,左右儿子,儿子数量,重复次数
}t[100005<<3];

void Update(int x)//维护儿子数量
{
	t[x].num_son=t[t[x].lson].num_son+t[t[x].rson].num_son+t[x].cnt;
}

int Identify(int x)//确定与父节点的关系
{
	return t[t[x].fa].lson==x?0:1;
	//其父亲的左儿子是不是自己,可以判断自己是不是左儿子
}

void Rotate(int x)
{
	int y=t[x].fa;//y是x原来的父亲
	int z=t[y].fa;//z是x原来的爷爷
	int k=Identify(x);//自己是左还是右儿子(下面的注释以右旋,即k=0为例)

	t[z].ch[Identify(y)]=x;//这个节点原来的爷爷的儿子是自己
	t[x].fa=z;//将这个节点的父亲设为原来自己的爷爷
	//上面两步即取代父亲的位置

	t[y].ch[k]=t[x].ch[k^1];//它的右儿子变为父亲的左儿子
	t[t[x].ch[k^1]].fa=y;//它左儿子的父亲是它右儿子的父亲,即统一父亲

	t[x].ch[k^1]=y;//它的右儿子是自己的父亲
	t[y].fa=x;//它原来的父亲变为了儿子

	Update(y);
	Update(x);//每次操作后需要维护
}

void Splay(int x,int goal)//把x节点旋转到某个位置
{
	while(t[x].fa!=goal)//如果还没达到目的
	{
		int y=t[x].fa;
		int z=t[y].fa;
		if(z!=goal)//如果它的爷爷不是目标
			(t[y].lson==x)^(t[z].lson==y)?Rotate(x):Rotate(y);
		Rotate(x);
	}
	if(!goal) root=x;
}

void Insert(int x)//插入一个节点x
{
	int u=root,fa=0;
	while(u&&t[u].v!=x)
	{
		fa=u;
		u=t[u].ch[x>t[u].v];
	}
	if(u) ++t[u].cnt;//如果已经有这个数字了,计数+1
	else//如果不存在这个数字,就加入新的节点
	{
		u=++tot;
		if(fa) t[fa].ch[x>t[fa].v]=u;
		t[tot].lson=0;
		t[tot].rson=0;
		t[tot].fa=fa;
		t[tot].v=x;
		t[tot].cnt=1;
		t[tot].num_son=1;
	}
	Splay(u,0);
}

void Find(int x)//查询x的排名
{
	int u=root;
	if(!u) return;//不存在节点,无法查找排名
	while(t[u].ch[x>t[u].v]&&x!=t[u].v)
		u=t[u].ch[x>t[u].v];
	Splay(u,0);
}

int Next(int x,int opt)//0为前驱,1为后继
{
	Find(x);
	int u=root;
	if((t[u].v>x&&opt)||(t[u].v<x&&!opt)) return u;
	u=t[u].ch[opt];
	while(t[u].ch[opt^1]) u=t[u].ch[opt^1];
	return u;
}

void Delete(int x)
{
	int last=Next(x,0);//x的前驱
	int next=Next(x,1);//x的后继
	Splay(last,0);Splay(next,last);
	int del=t[next].lson;
	if(t[del].cnt>1)
	{
		--t[del].cnt;//存在多个的数字被删去,计数-1
		Splay(del,0);
	}
	else t[next].lson=0;//清除节点
}

int Kth(int x)//查询第k名是哪个数
{
	int u=root;
	if(t[u].num_son<x) return false;//没有这么多数
	while(1)
	{
		int y=t[u].lson;
		if(x>t[y].num_son+t[u].cnt)//在排名为u的数的后面
		{
			x-=t[y].num_son+t[u].cnt;//减去那么多
			u=t[u].rson;//在右子树中找
		}
		else if(t[y].num_son>=x) u=y;//如果y的节点多于x,在左子树中找
		else return t[u].v;//找到了,返回结果
	}
}

template<class T>inline void read(T &res)
{
	T flag=1;char c;
	while((c=getchar())<'0'||c>'9')if(c=='-')flag=-1;res=c-'0';
	while((c=getchar())>='0'&&c<='9')res=(res<<1)+(res<<3)+c-'0';res*=flag;
}

int main()
{
	Insert(-1234567890);
	Insert(+1234567890);

	read(T);
	while(T--)
	{
		read(opt);
		if(opt==1)
		{
			read(x);
			Insert(x);
		}
		else if(opt==2)
		{
			read(x);
			Delete(x);
		}
		else if(opt==3)
		{
			read(x);
			Find(x);
			printf("%d
",t[t[root].lson].num_son);
		}
		else if(opt==4)
		{
			read(x);
			printf("%d
",Kth(x+1));
		}
		else if(opt==5)
		{
			read(x);
			printf("%d
",t[Next(x,0)].v);
		}
		else if(opt==6)
		{
			read(x);
			printf("%d
",t[Next(x,1)].v);
		}
	}
	return 0;
}

放到最后,其实本题还有(STL)的解法,就像下面这样

#include <bits/stdc++.h>
using namespace std;
int main()
{
	vector<int> A;
	int T;
	cin>>T;
	while(T--)
	{
		int opt,x;
		cin>>opt>>x;
		int pos=lower_bound(A.begin(),A.end(),x)-A.begin();
		switch(opt)
		{
			case 1:A.insert(A.begin()+pos,x);break;
			case 2:A.erase(A.begin()+pos,A.begin()+pos+1);break;
			case 3:cout<<pos+1<<endl;break;
			case 4:cout<<A[x-1]<<endl;break;
			case 5:cout<<A[pos-1]<<endl;break;
			case 6:cout<<*upper_bound(A.begin(),A.end(),x)<<endl;break;
		}
	}
	return 0;
}

缺点是,这种方法所用时间是普通平衡树的五倍,因此非常不推荐使用!(实在不行拿来骗分是可以的)


END


鸣谢:

绘图:tqr

代码:tqr

讲解:tqr

……( iny ext{我鸣谢我自己})……

https://home.cnblogs.com/u/tqr06/

https://www.cnblogs.com/tqr06/p/10400144.html

原文地址:https://www.cnblogs.com/tqr06/p/11169859.html