【数据结构】图文详解Treap平衡树

一、前言

Treap平衡树是一种树形数据结构,它是对二叉查找树的一种优化方式,但是学习的过程是真的非常的…痛苦。

二、二叉查找树的概念

首先,我们称具有以下3个性质的树为二叉查找树(BST)
1.左子树上所有结点的值均小于或等于它的根结点的值。
2.右子树上所有结点的值均大于或等于它的根结点的值。
3.左、右子树也分别为二叉排序树。
如图
在这里插入图片描述
在这张图中我们可以发现,在二叉查找树中,从根结点开始查找一个点的时间复杂度为logn级别,在我们想对树进行插入查找的时候都非常方便。

但是很遗憾,二叉查找树是有着它的缺陷的,例如:假如我们最开始有一个这样的树
在这里插入图片描述

然后我们如果不断插入比6小的数,那么二叉查找树就会变成…
在这里插入图片描述
一点都不好看!而且这样的话,我们查找的时间复杂度就又变成了n了,二叉查找树就失去了优势。

三、Treap平衡树的概念

对于上面那张图,我们觉得它一点也不方便,也不好看,我们希望那棵树是这个样子的
在这里插入图片描述
即使不能变成上图那种最完美的树,但是我们也还是想要去达到这个效果

然后,我们了解一下树的旋转

下面这张图就是把节点4右转后形成的新图,仍然符合二叉查找树的定义,左转同理
在这里插入图片描述
旋转又什么用呢?我们先再看看那张不漂亮的图
在这里插入图片描述
如果我们以节点5右转后会发生什么呢?
在这里插入图片描述
唉?好像好看多了,那这样的话岂不是我们多旋转几次不就越来越好看了吗?
但是我们需要思考,我们要怎么样才能让它转起来呢?

对此,我们实现的方法是:每一个点,我们用结构体除了封装它的左右儿子以及自己的值以外,我们还封装一个变量val,val的值不是我们决定的,而是在定义点的时候由rand()函数随机赋予一个数。我们规定在我们的树中,上面的节点的val值必须要大于下面的节点的val值,我们在建树,插入数字的时候要不断检查,一旦val值大的节点在val值小的节点的下方,我们就要通过旋转来让val值满足要求。

通过这样的方法,只要val值足够随机,那么我们就能保证我们的树不会出现上文那种特别难看的情况,从而增加我们的查找以及修改效率。

例如,假如现在我们的树是这样的,红字代表val值
在这里插入图片描述
我们要插入一个值为4的点,val值被rand()函数随机赋值为30,我们先把它插进去
在这里插入图片描述
很明显,点4和5的val值不符合要求,以5右转
在这里插入图片描述
点4和6依然不符合,继续以6右转,就会变成这样
在这里插入图片描述
唉?好像树的高度是没有变的啊!那我们的辛苦是为了什么?别急,如果我们要再加入一个点3,val为21,图会变成这样
在这里插入图片描述
而如果不处理,我们的树会变成这样
在这里插入图片描述
从这里我们就可以看出,我们的操作是有意义的,它成功的“美化”了我们的树。
再次重复,只要赋予的val值足够随机,那么一棵树在操作中就会维持一个比较美丽的状态,而rand()函数是可以提供足够随机数的,这就让我们Treap平衡树的实现有了保证。

三、Treap平衡树的代码实现

例题链接:Acwing 普通平衡树

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

插入数值 x。
删除数值 x(若有多个相同的数,应只删除一个)。
查询数值 x 的排名(若有多个相同的数,应输出最小的排名)。
查询排名为 x 的数值。
求数值 x 的前驱(前驱定义为小于 x 的最大的数)。
求数值 x 的后继(后继定义为大于 x 的最小的数)。
注意: 数据保证查询的结果一定存在。

输入格式
第一行为 n,表示操作的个数。

接下来 n 行每行有两个数 opt 和 x,opt 表示操作的序号(1≤opt≤6)。

输出格式
对于操作 3,4,5,6 每行输出一个数,表示对应答案。

数据范围
1≤n≤100000,所有数均在 −1e7 到 1e7 内。

输入样例:
8
1 10
1 20
1 30
3 20
4 2
2 10
5 25
6 -1
输出样例:
2
20
20
20

这题的代码真的真的真的真的很难写!!!!!而且由于代码量的巨大导致调试起来非常非常非常非常的痛苦!!!!这道题是需要足够的耐心去写的。具体思路在代码中会解释。

看懂代码你需要知道以下知识点
1.结构体的简单操作
2.rand函数的使用
3.耐心
4.耐心
5.还**的是耐心

//#pragma GCC optimize(2)
#include<iostream>
#include<iomanip>
#include<cstdio>
#include<string>
#include<algorithm>
#include<cmath>
#include<queue>
#include<vector>
#include<map>
#include<stack>
#include<set>
#include<bitset>
#include<ctime>
#include<cstring>
#include<list>
#define ll long long
#define ull unsigned long long
#define INF 0x3f3f3f3f
#define mem(a,b) memset(a,b,sizeof(a))
using namespace std;
typedef  pair<int, int> PII;
const int N = 1e6 + 7;
int n;

struct Node
{
	int l, r;  //左右节点编号
	int k;  //键值
	int val;  //随机分配的堆中的编号,不断旋转保证编号大的点一定在编号小的点的上方
	int cnt, size;   //cnt代表k有多少个,size代表以u为根的树有多少个点
}tr[N];

int root, idx;

void pushup(int u)
{
	tr[u].size = tr[tr[u].l].size + tr[tr[u].r].size + tr[u].cnt;
}

int new_node(int k)  //新建点
{
	tr[++idx].k = k;
	tr[idx].val = rand();  //rand函数赋予随机值
	tr[idx].cnt = 1;
	tr[idx].size = 1;
	return idx;  //返回节点编号方便递归建树
}

void zag(int& u)  //左旋函数,看不懂可以画图理解一下
{
	int q = tr[u].r;
	tr[u].r = tr[q].l;
	tr[q].l = u;
	u = q;
	pushup(tr[u].l);
	pushup(u);
}

void zig(int& u)  //右转函数
{
	int q = tr[u].l;
	tr[u].l = tr[q].r;
	tr[q].r = u;
	u = q;
	pushup(tr[u].r);
	pushup(u);
}

void build()  //建树,树的初始化
{
	new_node(-INF), new_node(INF);  //防止越界的哨兵
	root = 1, tr[1].r = 2;
	pushup(root);
	if (tr[1].val < tr[2].val) zag(root);
}

void del(int& u, int k)  //树的删除操作
{
	if (u == 0) return;  //没有这个点就不操作
	if (tr[u].k == k)  //找到了这个点
	{
		if (tr[u].cnt > 1) tr[u].cnt--;  //这个点的数量大于1就直接减去一个就可以了
		else
		{
			if (tr[u].l || tr[u].r)  //不是叶子节点
			{
				if (!tr[u].r || tr[tr[u].l].val)  //如果左边有点就把要删除的点通过右转转到右边去,然后通过递归不断旋转直到转到叶子节点后删除
				{
					zig(u);
					del(tr[u].r, k);
				}
				else  //否则转到左边去
				{
					zag(u);
					del(tr[u].l, k);
				}
			}
			else   //是叶子节点就可以直接删除了
				u = 0;
		}
	}
	else if (tr[u].k > k) del(tr[u].l, k);//如果没有找到就判断一下在左右两边的哪一边
	else del(tr[u].r, k);//找一下
	pushup(u);//上传更改
}

void insert(int& u, int k)  //点的插入操作
{
	if (u == 0) u = new_node(k);  //走到0说明当前位置没有点,就把点插在这里
	else
	{
		if (tr[u].k == k)   //重复点数量直接加1
			tr[u].cnt++;
		else
		{
			if (tr[u].k > k)  //比当前点的键值大就插入左边
			{
				insert(tr[u].l, k);
				if (tr[tr[u].l].val > tr[u].val) zig(u);  //平衡旋转
			}
			else   //否则插入右边
			{
				insert(tr[u].r, k);
				if (tr[tr[u].r].val > tr[u].val) zag(u);  //平衡旋转
			} 
		}
	}
	pushup(u);//更新节点信息
}

int get_rank(int u, int k)  //求k的位置
{
	if (u == 0) return 0;//是0随便返回就行
	if (tr[u].k == k) return tr[tr[u].l].size + 1;//相等了那排名应该就是左边的数量加上自己
	if (tr[u].k > k) return get_rank(tr[u].l, k);//大了找左边
	return tr[tr[u].l].size + tr[u].cnt + get_rank(tr[u].r, k);//找右边
}

int get_key(int u, int rank)  //求该位置的值
{
	if (u == 0) return INF;
	if (tr[tr[u].l].size >= rank) return get_key(tr[u].l, rank);//找左边
	if (tr[tr[u].l].size + tr[u].cnt >= rank) return tr[u].k;//如果满足条件就直接return
	return get_key(tr[u].r, rank - tr[tr[u].l].size - tr[u].cnt);//不然就找右边
}

int get_pr(int u, int k)//前驱,小于k的最大的数
{
	if (u == 0) return -INF;
	if (tr[u].k >= k) return get_pr(tr[u].l, k);//找左边
	return max(get_pr(tr[u].r, k), tr[u].k);//可能是右边可能是这个数,所以用个max
}

int get_ne(int u, int k)//后继,大于k的最小的数
{
	if (u == 0) return INF;//后继的写法和前驱相反,大家可以注意一下
	if (tr[u].k <= k) return get_ne(tr[u].r, k);
	return min(get_ne(tr[u].l, k), tr[u].k);
}

void solve()
{
	build();
	cin >> n;
	while (n--)
	{
		int op, x;
		cin >> op >> x;
		switch (op)
		{
		case 1:
		{
			insert(root, x);
			break;
		}
		case 2:
		{
			del(root, x);
			break;
		}
		case 3:
		{
			cout << get_rank(root, x) - 1 << endl;
			break;
		}
		case 4:
		{
			cout << get_key(root, x + 1) << endl;
			break;
		}
		case 5:
		{
			cout << get_pr(root, x) << endl;
			break;
		}
		case 6:
		{
			cout << get_ne(root, x) << endl;
			break;
		}
		}
	}
}

int main()
{
	//std::ios::sync_with_stdio(false);
	//cin.tie(0), cout.tie(0);
	solve();
	return 0;
}

作者:Avalon Demerzel,喜欢我的博客就点个赞吧,更多图论与数据结构知识点请见作者专栏《图论与数据结构》

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值