Treap基本原理
一、Treap的特性
与、红黑树等平衡树本质相同,都是一个二叉查找树()。但是作为一个平衡树,它必须要有一个维护树平衡的功能(避免变成一条链)。它的每个节点还有一个随机生成的优先级,这些优先级要满足堆的性质,以保证这个树相对较平衡。
比如说这个:

就是一个树(本质上跟没区别)
问题是,在调整(插入、删除元素)树时可能会使得每个节点的优先级不满足堆的性质,所以我们要对树进行调整。
二、Treap的操作
我们为了保证树在改变后优先级依然能够满足堆的性质,我们需要在它满足二叉查找树的前提下进行旋转使得它的优先级形成一个堆。的好处就在于它只有种旋转。
右旋


我们假设这个树已经满足二叉查找树的特性,即,我们可以这样旋转
第一步:把边挂到下面

第二步:把点挂到下面

这样旋转,依然保证这个子树满足,还调整了树形。
左旋

左旋跟右旋差不多一样,把右旋的整个过程反过来就是左旋。也是分两步走:

第一步:

第二步:

插入节点
也是一类,所以插入的时候我们首先要遵循的插入规则,插入之后再根据优先级判断是否需要旋转。我们以这个树为例(绿色小字是该节点的优先级),我们要在这个树中插入一个。

当前的目标是在以值为的节点为根的子树上,插入一个值为8的节点。
而我们发现,,一定在根的右子树上。
因此,这个问题就变成了在以值为的节点为根的子树上插入一个值为的节点。
而我们发现,,一定在根的右子树上。
这个问题就变成了在以值为的节点为根的子树上,插入一个值为的节点。
而我们发现,,一定在根的左子树上,而已经是叶子了,可以直接往进塞。
假设这个节点的优先级是(随机出来的):

很明显,两个标红的优先级不满足大顶堆的特性(即儿子的优先级大于父亲的了),而且这两个节点是向左斜的,那么我们就要对这个节点进行右旋。因为两个节点都没有额外的儿子,所以一步完成:

显然,旋转以后这依然满足的特性。然而,我们又发现,两个标红的优先级不满足堆的特性了,而且这两个不满足的节点是向右斜的,我们可以对这个子树进行左旋:

一次插入就完成啦!
我们总结一下,一步步往下走找到一个合适的位置满足,然后再一步步往上走进行旋转以满足堆的特性。
显然,我们可以用递归来完成这个过程。往下走的部分借助递,向上走的部分借助归。
删除节点
删除节点与插入节点的顺序基本一样。都是先下后上。
我们还是举一个例子,我们要删除值为的节点:

当前的目标就是在以值为的节点为根的子树中删除值为的节点
因为,所以目标节点一定在根的右子树上,这个问题就变为,
在以值为的节点为根的子树中删除值为的节点
很好!我们已经找到目标节点了。皇帝驾崩了,大皇子顶上!
我们可以让树旋转使得优先级较大的儿子替换掉父亲(目标节点)
在这里我们给这个子树进行左旋

但是我们要删的节点跑了,我们要继续追杀!
我们追到左子树,拿着枪对着它,它还算敬业,要让自己另一个儿子接班后再阵亡。
作为一个善良的人,我们只好应允它的请求,再它给转!

这时候,已经没有拖延时间的借口了,我们直接祝它清明节快乐,删掉它吧!

显然删完之后,这个树依然满足树的特性。
查询
查询分好几种,因为跟普通一样,所以直接贴代码了。
三、总结
不算是一个标准的平衡树。但因为它完美地结合了树和堆的特性,使得它常数比小,无论是在竞赛中还是在开发应用中都有比较好的效果,因此常用来代替树。
同时,我们也可以从中学到一点:两种不同的算法可以通过巧妙的方法优势互补,从而达到更好的效果。在实际开发中我们如果能运用这个方法,一定能得到不小的成效。
四、代码模板
P3369 【模板】普通平衡树
AcWing 253 普通平衡树
#include <bits/stdc++.h>
using namespace std;
const int N = 100010;
const int INF = 0x3f3f3f3f;
mt19937 rnd(time(0)); //高性能随机数生成器 随机范围大概在(?maxint,+maxint),233为种子
int n;
struct Node {
int l, r; //左儿子右儿子的节点号
int val; //在BST中的数值
int fix; //堆中的编号,修正值
int cnt; //当前节点是数字的个数
int size; //以当前节点为根的子树中数字的总个数
} tr[N];
//比如tr[1] 就是1号节点,一般用来放根
int root, idx;
//上传节点信息,更新size
void pushup(int p) {
//递归统计,左儿子数量+右儿子数量+自己本身节点上记录的数量
tr[p].size = tr[tr[p].l].size + tr[tr[p].r].size + tr[p].cnt;
}
//创建新点,k为在BST中的数值
int newnode(int val) {
tr[++idx].val = val; //新开一个空间++idx,值k=k
tr[idx].fix = rnd(); //尽量随机,随手给个就行
tr[idx].cnt = tr[idx].size = 1; //叶子节点,所以此位置的数字个数为1,以它为根的子树中所有数字个数和也为1
return idx; //返回节点号
}
//左右旋,没啥好说的,自己在纸上画一下就知道了
void zig(int &p) {
int q = tr[p].l;
tr[p].l = tr[q].r;
tr[q].r = p;
p = q;
pushup(tr[p].r);
pushup(p); //最后一定要记得上传,不然完了
}
void zag(int &p) {
int q = tr[p].r;
tr[p].r = tr[q].l;
tr[q].l = p;
p = q;
pushup(tr[p].l);
pushup(p);
}
//建树操作,为了正确性增加两个哨兵,防止越界
void build() {
newnode(-INF), newnode(INF); //初始化两个哨兵,一个值是-INF,一个值是INF,利用左右旋打开局面
root = 1, tr[1].r = 2; //初始化一下,根是1号结点,根的右儿子是2号节点
pushup(root); //上传信息
/*
x y
/ \ 右旋zig / \
y tt -> hh x
/ \ <- / \
hh z 左旋zag z tt
*/
if (tr[1].fix < tr[2].fix) zag(root); //不平衡了就左旋一下,这两个点也要PK一下大小王,如上图,可理解为将右儿子变根
}
//在以u为根的子树中插入一个数字k,因为根可能在操作过程中左旋或右旋等变更根,所以u就以引用方式传入的
void insert(int &p, int val) {
if (!p)
p = newnode(val); //如果走到空了,就新建一个值为k的节点,并记录到u中,u似乎更像一个游标
else {
if (tr[p].val == val)
tr[p].cnt++; //如果找到了相同的节点,就cnt++
else {
if (tr[p].val > val) { //看看是在左边还是在右边
insert(tr[p].l, val); //准备向左边递归执行插入动作
if (tr[tr[p].l].fix > tr[p].fix) zig(p); //插入完,不平衡立马调整,左侧插入右旋
} else {
insert(tr[p].r, val); //准备向右边递归执行插入动作
if (tr[tr[p].r].fix > tr[p].fix) zag(p); //插入完,不平衡立马调整,右侧插入左旋
}
}
}
pushup(p); //最后上传一下,是不是和线段树有点像啊?
}
//删除操作
void remove(int &p, int val) {
if (p == 0) return; //如果没了说明节点不存在,就不管了。
if (tr[p].val == val) { //如果找到了这个点
if (tr[p].cnt > 1)
tr[p].cnt--; //大于一好说,直接cnt --
else { //不大于一
if (tr[p].l || tr[p].r) { //先看看是不是叶节点
if (!tr[p].r || tr[tr[p].l].fix) { //如果右儿子为空,或者 左侧随机值不为零
zig(p); //右旋
remove(tr[p].r, val); //在右边删除k
} else {
zag(p); //左旋
remove(tr[p].l, val); //在左边删除k
}
} else
p = 0; //直接删除
}
} else if (tr[p].val > val)
remove(tr[p].l, val); //在左侧删除
else
remove(tr[p].r, val); //在右侧删除
pushup(p); //上传更改
}
//获取k的排名,查询时,不需要修改游标u,不用按&地址符传递引用
int get_rank_by_val(int p, int val) {
if (!p) return 0; //是0随便返回就行
if (tr[p].val == val) return tr[tr[p].l].size + 1; //相等了那排名应该就是左边的数量加上自己
if (tr[p].val > val) return get_rank_by_val(tr[p].l, val); //大了找左边
return tr[tr[p].l].size + tr[p].cnt + get_rank_by_val(tr[p].r, val); //找右边
}
//按rank排名查询值
int get_val_by_rank(int p, int rank) {
if (!p) return 0;
if (tr[tr[p].l].size >= rank) return get_val_by_rank(tr[p].l, rank); //找左边
if (tr[tr[p].l].size + tr[p].cnt >= rank) return tr[p].val; //如果满足条件就直接return
return get_val_by_rank(tr[p].r, rank - tr[tr[p].l].size - tr[p].cnt); //不然就找右边
}
int get_prev(int p, int val) {
if (!p) return -INF;
if (tr[p].val >= val) return get_prev(tr[p].l, val);
return max(get_prev(tr[p].r, val), tr[p].val);
}
// 求数值 x 的后继(后继定义为大于 x 的最小的数)。
// 后继:找到严格大于val的最小数
int get_next(int p, int val) {
if (!p) return INF; //后继的写法和前驱相反,大家可以注意一下
if (tr[p].val <= val) return get_next(tr[p].r, val);
return min(get_next(tr[p].l, val), tr[p].val);
}
int main() {
build(); //建树,要是忘了就凉了
scanf("%d", &n);
while (n--) {
int op, x;
scanf("%d%d", &op, &x);
if (op == 1) //插入一个数x
insert(root, x);
else if (op == 2) //删除一个数x
remove(root, x);
else if (op == 3)
printf("%d\n", get_rank_by_val(root, x) - 1); //获取数x的排名,因为有两个哨兵,一前一后,所以前面有1个
else if (op == 4)
printf("%d\n", get_val_by_rank(root, x + 1)); //按排名查值,因为有一个前置哨兵,在Treap中,实际的排名需要+1
else if (op == 5)
printf("%d\n", get_prev(root, x)); //获取x的前驱
else
printf("%d\n", get_next(root, x)); //获取x的后继
}
return 0;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· Docker 太简单,K8s 太复杂?w7panel 让容器管理更轻松!
2018-05-10 今天需要完成的开发任务
2013-05-10 使用PowerDesigner生成数据库测试数据
2013-05-10 开发流程与各层软件选型
2013-05-10 MySQL分区和分布性能测试[转]
2013-05-10 Innodb共享表空间VS独立表空间
2013-05-10 MySQL MyISAM与Innodb优化方案比较