学习笔记:Treap
Treap
引入
Treap 是一种弱平衡的二叉搜索树。它同时满足二叉搜索树和堆的相关性质。从某种意义上讲:
前置知识
二叉搜索树
二叉搜索树是一种二叉树的树形数据结构,其定义如下:
- 空树是二叉搜索树。
- 若二叉搜索树的左子树不为空,则其左子树上所有点的附加权值均小于其根节点的值。
- 若二叉搜索树的右子树不为空,则其右子树上所有点的附加权值均大于其根节点的值。
- 二叉搜索树的左右子树均为二叉搜索树。
二叉搜索树上的基本操作所花费的时间与这棵树的高度成正比。对于一个有 个结点的二叉搜索树中,这些操作的最优时间复杂度为 ,最坏为 。随机构造这样一棵二叉搜索树的期望高度为 。
堆
堆是一棵树,其每个节点都有一个键值,且每个节点的键值都大于等于/小于等于其父亲的键值。
每个节点的键值都大于等于其父亲键值的堆叫做小根堆,否则叫做大根堆。STL 中的 priority_queue
其实就是一个大根堆。
堆主要支持的操作有:插入一个数、查询最小值、删除最小值、合并两个堆、减小一个元素的值。
一些功能强大的堆(可并堆)还能(高效地)支持 merge 等操作。一些功能更强大的堆还支持可持久化,也就是对任意历史版本进行查询或者操作,产生新的版本。
实现
二叉搜索树的性质是:
- 左子节点的值比父节点大
- 右子节点的值比父节点小(当然这也是可以反过来的)
堆的性质是:
- 子节点值比父节点大或小(取决于是小根堆还是大根堆)
不难发现,如果用的是同一个值,那这两种数据结构的性质是矛盾的,所以我们在维护搜索树权值 的基础上,再引入一个给堆的值 。对于 值,我们维护搜索树的性质,对于 值,我们维护堆的性质。其中 这个值是随机给出的。
那我们为什么需要大费周章的去让这个数据结构符合树和堆的性质,并且随机给出堆的值呢?
要理解这个,首先需要理解朴素二叉搜索树的问题。在给朴素搜索树插入一个新节点时,我们需要从这个搜索树的根节点开始递归,如果新节点比当前节点小,那就向左递归,反之亦然。最后当发现当前节点没有子节点时,就根据新节点的值的大小,让新节点成为当前节点的左或右子节点。
如果新插入的节点的值是随机的,那这个朴素搜索树的形状会非常的「胖」,也就是说,每一层的节点比较多。在这样的情况下,这个搜索树的层数是会比较接近 ( 为节点数)的,查询的复杂度也是(因为只要递归这么多层就能查到)。
不过,这只是在随机情况下的复杂度,如果我们按照一个非常有序的顺序给一个朴素的搜索树插入节点。这个树就会变得非常「瘦长」(每次插入的节点都比前面的大,所以都被安排到右子节点了)。这时二叉搜索树已经退化成链了,查询的复杂度也从 变成了 。
而 Treap 要解决的正是这个问题。它通过随机化的 属性,以及维护堆性质的过程,「打乱」了节点的插入顺序。从而让二叉搜索树达到了理想的复杂度,避免了退化成链的问题。
旋转 Treap
旋转
旋转操作是 Treap 的一个非常重要的操作,主要用来在保持 Treap 树性质的同时,调整不同节点的层数,以达到维护堆性质的作用。
旋转操作的含义:
- 在不影响搜索树性质的前提下,把和旋转方向相反的子树变成根节点(如左旋,就是把右子树变成根节点)。
- 不影响性质,并且在旋转过后,跟旋转方向相同的子节点变成了原来的根节点(如左旋,旋转完之后的左子节点是旋转前的根节点)。
左旋和右旋操作是相互的。
void zag(int &node){ // 左旋
int right = a[node].r;
a[node].r = a[right].l;a[right].l = node;node = right;
update(a[node].l);update(node);
}
void zig(int &node){ // 右旋
int left = a[node].l;
a[node].l = a[left].r;a[left].r = node;node = left;
update(a[node].r);update(node);
}
建立、维护
建立主要是插入两个无穷大和无穷小的值(相对于题目具体数据范围而言)。
维护只需要更新一下子树大小。
void build(){
add(-INF);add(INF);
root = 1;a[1].r = 2;
update(root);
}
void update(int node){
a[node].siz = a[a[node].l].siz + a[a[node].r].siz + a[node].cnt;
}
插入、删除
插入跟普通搜索树插入的过程没啥区别,但是需要在插的过程中通过旋转来维护树堆中堆的性质。
删除主要就是分类讨论,不同的情况有不同的处理方法,删完了树的大小会有变化,要注意更新。并且如果要删的节点有左子树和右子树,就要考虑删除之后让谁来当父节点(维护 rank 小的节点在上面)。
void insert(int &node, int val){
if(node == 0){node = add(val);return;}
if(val == a[node].val)a[node].cnt++;
else if(val < a[node].val){
insert(a[node].l, val);
if(a[node].hap < a[a[node].l].hap)zig(node);
}else{
insert(a[node].r, val);
if(a[node].hap < a[a[node].r].hap)zag(node);
}
update(node);
}
void remove(int &node, int val){
if(node == 0)return;
if(val == a[node].val){
if(a[node].cnt > 1){
a[node].cnt--;update(node);
return;
}
else if(a[node].l != 0 || a[node].r != 0){
if(a[node].r == 0 || a[a[node].l].hap > a[a[node].r].hap){
zig(node);remove(a[node].r, val);
}else{
zag(node);remove(a[node].l, val);
}
update(node);
}else node = 0;
return;
}
if(val < a[node].val)remove(a[node].l, val);
else remove(a[node].r, val);
update(node);
}
根据权值查询排名、根据排名查询权值
根据权值查询排名只需要查询以 为根节点的子树中, 这个值的大小的排名(该子树中小于 的节点的个数 + 1)。
要根据排名查询权值,我们首先要知道如何判断要查的节点在树的哪个部分:
注意如果在右子树,递归的时候需要对原来的 rank
进行处理。递归的时候就相当去查,在右子树中为这个排名的值,为了把排名转换成基于右子树的,需要把原来的 rank
减去左子树的大小和根节点的重复次数。
可以把所有节点想象成一个排好序的数组,或者数轴。这里的转换方法就是直接把排名减去左子树的大小和根节点的重复数量。
int getrank(int node, int val){
if(node == 0)return 1;
if(val == a[node].val)return a[a[node].l].siz + 1;
else if(val < a[node].val)return getrank(a[node].l, val);
else return getrank(a[node].r, val) + a[a[node].l].siz + a[node].cnt;
}
int getval(int node, int rank){
if(node == 0)return INF;
if(a[a[node].l].siz >= rank)return getval(a[node].l, rank);
else if(a[a[node].l].siz + a[node].cnt >= rank)return a[node].val;
else return getval(a[node].r, rank - a[a[node].l].siz - a[node].cnt);
}
求前驱、后继
求前驱用到了一个变量 ,这个值是只有在 比当前节点值大的时候才会被更改的,所以返回这个变量就是返回 最后一次比当前节点的值大,之后就是更小了。
求后继跟前一个很相似,只是大于小于号换了一下。
int getpre(int val){
int ans = 1, node = root;
while(node != 0){
if(val == a[node].val){
if(a[node].l > 0){
node = a[node].l;
while(a[node].r > 0)node = a[node].r;
ans = node;
}
break;
}
if(a[node].val < val && a[node].val > a[ans].val)ans = node;
if(val < a[node].val)node = a[node].l;
else node = a[node].r;
}
return a[ans].val;
}
int getnxt(int val){
int ans = 2, node = root;
while(node != 0){
if(val == a[node].val){
if(a[node].r > 0){
node = a[node].r;
while(a[node].l > 0)node = a[node].l;
ans = node;
}
break;
}
if(a[node].val > val && a[node].val < a[ans].val)ans = node;
if(val < a[node].val)node = a[node].l;
else node = a[node].r;
}
return a[ans].val;
}
板子题(洛谷 P3369)
#include <iostream>
#include <cstdlib>
#define MAXN 100005
#define INF 0x7fffffff
using namespace std;
int n, opt, x;
struct Treap{
int l, r, val, hap, cnt, siz;
}a[MAXN];
int tot, root;
int read(){
int t = 1, x = 0;char ch = getchar();
while(!isdigit(ch)){if(ch == '-')t = -1;ch = getchar();}
while(isdigit(ch)){x = (x << 1) + (x << 3) + (ch ^ 48);ch = getchar();}
return x * t;
}
void write(int x){
if(x < 0){putchar('-');x = -x;}
if(x >= 10)write(x / 10);
putchar(x % 10 ^ 48);
}
int add(int val){
tot++;a[tot].val = val;
a[tot].hap = rand();
a[tot].cnt = 1;a[tot].siz = 1;
return tot;
}
void update(int node){
a[node].siz = a[a[node].l].siz + a[a[node].r].siz + a[node].cnt;
}
void zag(int &node){
int right = a[node].r;
a[node].r = a[right].l;a[right].l = node;node = right;
update(a[node].l);update(node);
}
void zig(int &node){
int left = a[node].l;
a[node].l = a[left].r;a[left].r = node;node = left;
update(a[node].r);update(node);
}
void build(){
add(-INF);add(INF);
root = 1;a[1].r = 2;
update(root);
}
void insert(int &node, int val){
if(node == 0){node = add(val);return;}
if(val == a[node].val)a[node].cnt++;
else if(val < a[node].val){
insert(a[node].l, val);
if(a[node].hap < a[a[node].l].hap)zig(node);
}else{
insert(a[node].r, val);
if(a[node].hap < a[a[node].r].hap)zag(node);
}
update(node);
}
void remove(int &node, int val){
if(node == 0)return;
if(val == a[node].val){
if(a[node].cnt > 1){
a[node].cnt--;update(node);
return;
}
else if(a[node].l != 0 || a[node].r != 0){
if(a[node].r == 0 || a[a[node].l].hap > a[a[node].r].hap){
zig(node);remove(a[node].r, val);
}else{
zag(node);remove(a[node].l, val);
}
update(node);
}else node = 0;
return;
}
if(val < a[node].val)remove(a[node].l, val);
else remove(a[node].r, val);
update(node);
}
int getrank(int node, int val){
if(node == 0)return 1;
if(val == a[node].val)return a[a[node].l].siz + 1;
else if(val < a[node].val)return getrank(a[node].l, val);
else return getrank(a[node].r, val) + a[a[node].l].siz + a[node].cnt;
}
int getval(int node, int rank){
if(node == 0)return INF;
if(a[a[node].l].siz >= rank)return getval(a[node].l, rank);
else if(a[a[node].l].siz + a[node].cnt >= rank)return a[node].val;
else return getval(a[node].r, rank - a[a[node].l].siz - a[node].cnt);
}
int getpre(int val){
int ans = 1, node = root;
while(node != 0){
if(val == a[node].val){
if(a[node].l > 0){
node = a[node].l;
while(a[node].r > 0)node = a[node].r;
ans = node;
}
break;
}
if(a[node].val < val && a[node].val > a[ans].val)ans = node;
if(val < a[node].val)node = a[node].l;
else node = a[node].r;
}
return a[ans].val;
}
int getnxt(int val){
int ans = 2, node = root;
while(node != 0){
if(val == a[node].val){
if(a[node].r > 0){
node = a[node].r;
while(a[node].l > 0)node = a[node].l;
ans = node;
}
break;
}
if(a[node].val > val && a[node].val < a[ans].val)ans = node;
if(val < a[node].val)node = a[node].l;
else node = a[node].r;
}
return a[ans].val;
}
int main(){
n = read();srand(time(0));build();
while(n--){
opt = read();x = read();
switch(opt){
case 1:insert(root, x);break;
case 2:remove(root, x);break;
case 3:write(getrank(root, x) - 1);putchar('\n');break;
case 4:write(getval(root, x + 1));putchar('\n');break;
case 5:write(getpre(x));putchar('\n');break;
case 6:write(getnxt(x));putchar('\n');break;
}
}
return 0;
}
无旋 Treap (fhq-Treap)
Treap——大名鼎鼎的随机二叉查找树,以优异的性能和简单的实现在 OIer 们中广泛流传。
这里介绍一种不需要旋转操作来维护的 Treap,即无旋 Treap,也称 fhq-Treap。
它的巧妙之处在于只需要分离和合并两种基本操作,就能实现任意的平衡树常用修改操作。
而不需要旋转的特性也使编写代码时不需要考虑多种情况和复杂的父亲儿子关系的更新,同时降低了时间复杂度。
此外,它还可以方便地支持可持久化,实在是功能强大。
fhq-Treap 主要有两种基本操作:分离(Split)和合并(Merge)。
分离:指的是将一棵 Treap 按照中序遍历的顺序,分割成左右两半,满足左右两半组成的 Treap 的所有值都不变。
合并:指的是将两棵 Treap(一般是从原先的 Treap 中 Split 出来的)合并在一起,按照中序遍历的顺序,并且所有节点的值都不变。
代码
#include <iostream>
#include <cstdlib>
#define MAXN 100005
using namespace std;
int n, opt, x, root, tot;
struct Treap{
int l, r, val, siz, hap;
}a[MAXN];
int read(){
int t = 1, x = 0;char ch = getchar();
while(!isdigit(ch)){if(ch == '-')t = -1;ch = getchar();}
while(isdigit(ch)){x = (x << 1) + (x << 3) + (ch ^ 48);ch = getchar();}
return x * t;
}
void write(int x){
if(x < 0){putchar('-');x = -x;}
if(x >= 10)write(x / 10);
putchar(x % 10 ^ 48);
}
int add(int val){
tot++;a[tot].hap = rand();
a[tot].siz = 1;a[tot].val = val;
return tot;
}
void update(int node){
a[node].siz = a[a[node].l].siz + a[a[node].r].siz + 1;
}
void splitsiz(int node, int siz, int &x, int &y){
if(node == 0){x = 0;y = 0;return;}
if(a[a[node].l].siz >= siz){
y = node;splitsiz(a[node].l, siz, x, a[node].l);
}else{
x = node;splitsiz(a[node].r, siz - a[a[node].l].siz - 1, a[node].r, y);
}
update(node);
}
void splitval(int node, int val, int &x, int &y){
if(node == 0){x = 0;y = 0;return;}
if(a[node].val > val){
y = node;splitval(a[node].l, val, x, a[node].l);
}else{
x = node;splitval(a[node].r, val, a[node].r, y);
}
update(node);
}
int merge(int x, int y){
if(x == 0 || y == 0)return x | y;
int ans;
if(a[x].hap > a[y].hap){
ans = x;a[x].r = merge(a[x].r, y);
}else{
ans = y;a[y].l = merge(x, a[y].l);
}
update(ans);return ans;
}
void insert(int val){
int x, y;
splitval(root, val - 1, x, y);
root = merge(merge(x, add(val)), y);
}
void remove(int val){
int x, y, z, t;
splitval(root, val - 1, x, y);
splitsiz(y, 1, z, t);
root = merge(x, t);
}
int getrank(int val){
int x, y, ans;
splitval(root, val - 1, x, y);
ans = a[x].siz + 1;
root = merge(x, y);
return ans;
}
int getval(int rank){
int x, y, z, t, ans;
splitsiz(root, rank - 1, x, y);
splitsiz(y, 1, z, t);
ans = a[z].val;
root = merge(merge(x, z), t);
return ans;
}
int getpre(int val){
int x, y, z, t, ans;
splitval(root, val - 1, x, y);
splitsiz(x, a[x].siz - 1, z, t);
ans = a[t].val;
root = merge(merge(z, t), y);
return ans;
}
int getnxt(int val){
int x, y, z, t, ans;
splitval(root, val, x, y);
splitsiz(y, 1, z, t);
ans = a[z].val;
root = merge(merge(x, z), t);
return ans;
}
int main(){
n = read();srand(time(0));
for(int i = 1 ; i <= n ; i ++){
opt = read();x = read();
switch(opt){
case 1:insert(x);break;
case 2:remove(x);break;
case 3:write(getrank(x));putchar('\n');break;
case 4:write(getval(x));putchar('\n');break;
case 5:write(getpre(x));putchar('\n');break;
case 6:write(getnxt(x));putchar('\n');break;
default:cout << "jtskdjfkxqsvivo50wdnc" << endl;break;
}
}
return 0;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】