一、前言
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,喜欢我的博客就点个赞吧,更多图论与数据结构知识点请见作者专栏《图论与数据结构》