平衡树之Treap—强大的数据结构

本文详细介绍了Treap及其可持久化版本的基本概念、实现方式与应用场景。Treap是一种结合了二叉搜索树和堆特性的数据结构,支持高效地插入、删除和查找操作。可持久化Treap通过添加分裂与合并操作,进一步增强了Treap的功能,能够实现区间修改和查询等复杂操作。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

Treap介绍

Treap,是平衡树的分支之一,故也支持旋转操作,在数据结构中也称树堆,之所以叫树堆,是因为Treap=Tree(树)+Heap(堆)。其基本操作的期望时间复杂度为O(log n)。相对于其他的平衡二叉搜索树,Treap的特点是实现简单,且能基本实现随机平衡的结构。

正题

Treap是一棵二叉排序树,它的左子树和右子树分别是一个Treap,和一般的二叉排序树不同的是,Treap记录一个额外的数据,就是优先级。
Treap里面的每一个元素都有一个随机的数值作为优先级,如果就优先级来看,Treap将会满足堆的性质(大根堆或小根堆)。由于优先级是随机出来的,因此期望树高为log n

构建

构建一棵Treap,有两种方法。
第一种是较为简单,将数按顺序加入,并给加入的元素附上一个随机优先级,在对元素进行插入操作即可。时间复杂度O(n log n)。
第二种就是直接维护一个栈。考虑到优先级会随着深度的增加而减小,因此可以维护一个栈来完成Treap的构建,一个元素入栈一次退栈一次,故时间复杂度是O(n)的,在这里就不细讲了。

如果实在不会第二种用第一种也无妨,毕竟要维护一棵Treap就意味着会有询问或修改操作,单次操作的期望复杂度是O(log n)的,因而总体来说总时间复杂度并不会有很大区别。

插入

首先先按照二叉搜索树的插入一样将元素插入到一个叶节点上,然后就从下往上维护堆的性质,利用旋转操作,如果儿子节点的优先级比父亲优的话就把儿子节点旋转至他父亲的位置。因为期望树高是log n的,所以期望时间复杂度O(log n)。
附上代码。

Code:

//tree[x].size是指以x为根的子树大小
//tree[x].v是指以x节点的实际数值
//tree[x].l和tree[x].r分别是指x的左儿子和右儿子
//tree[x].rnd是指x的随机优先级
//leftturn是左旋操作,rightturn是右旋操作
//接下来的都一样,就不加注释了
void insert(int &k,int x)//此Treap为小根堆
{
    if(!k)
    {
        k=++size;
        tree[k].size=1;tree[k].v=x;tree[k].rnd=rand();
        return;
    }
    tree[k].size++;
    else if(x>tree[k].v)
    {
        insert(tree[k].r,x);
        if(tree[tree[k].r].rnd<tree[k].rnd)leftturn(k);//维护堆性质
    }
    else 
    {
        insert(tree[k].l,x);
        if(tree[tree[k].l].rnd<tree[k].rnd)rightturn(k);
    } 
}

删除

先找到要删除的那个节点,然后把该节点优先级较大的儿子旋转到自己的位置,直到该节点选转到了叶节点的位置,便可以直接删除了。期望时间复杂度O(log n)。
附上代码。

Code:

void del(int &k,int x)
{
    if(!k)return; 
    if(tree[k].v==x)
    {
        if(tree[k].l*tree[k].r==0)k=tree[k].l^tree[k].r;//有一个儿子为空
        else if(tree[tree[k].l].rnd<tree[tree[k].r].rnd)
            rightturn(k),del(k,x);
        else leftturn(k),del(k,x);
    }
    else if(x>tree[k].v)
        tree[k].size--,del(tree[k].r,x);
    else tree[k].size--,del(tree[k].l,x);
}

查找

查找和一般的二叉排序树一样,但是由于Treap的随机化结构,Treap中查找的期望复杂度是O(log n)。

区别

Treap比其他的平衡树更易实现,细节更少,易调试,效率上来说也不比其他的平衡树差。

其次就是Treap的结构不是固定的,而像splay等平衡树的结构在数据相同的情况下结构的变化都是相同的,而Treap对于相同的数据结构不太可能相同,因此如果存在能够卡掉像splay等平衡树的情况,Treap则不会因此而被卡掉,虽然目前为止我也没有见过这种情况。

再然后,splay的时间复杂度是 均摊 log n的,而Treap的时间复杂度是期望 log n,这之间还是有挺大的区别的。

总而言之,Treap与其他平衡树区别的本质在于Treap加入了随机化。

可持久化Treap

如果你认真研究Treap,你就会发现普通的Treap大多数时候都不能维护别的平衡树能够维护的操作,例如区间翻转操作,因而就出现了可持久化Treap
可持久化Treap有两个基本操作——分离(split)和合并(merge),在这两个操作的基础上Treap就能维护更多其他的操作。

分离(split)

分离操作会将一棵Treap分离成两棵Treap,一般为将一棵大小的sizeTreap分成前k个和后size-k个,分为两棵Treap,因而split函数返回值类型为pair
具体实现如下,每次要从以o为根的Treap分成前k个和后tree[o].size-k个,先分类讨论,判断分离位置位于左子树还是右子树,递归进行,直至k0。期望复杂度为树高,即O(log n)。

看看代码理解会更好。

Code:

//change(o)是指重新计算节点o的信息(更新节点o的信息)
//down(o)是指将o的标记下传至儿子节点
//下面merge也是一样

#define fi first
#define se second
typedef pair<int,int> P;

P split(int o,int k)
{
    if(!k)return P(0,o);
    down(o);
    if(size[tree[o].l]>=k){
        P ls=split(tree[o].l,k);
        tree[o].l=ls.se;
        change(o);
        return P(ls.fi,o);
    }
        P ls=split(tree[o].r,k-tree[tree[o].l].size-1);
        tree[o].r=ls.fi;
        change(o);
        return P(o,ls.se);
}

合并(merge)

函数merge有两个自变量,分别为需要被合并的两棵Treap的根节点,返回值x表示当前合并完的Treap的根节点编号。合并过程也不难,每次合并从两个根中选择优先级较大的节点作为新的根节点,这时问题就转换为另外两棵Treap合并的问题了。
期望复杂度为树高,即O(log n)。

还是看看代码好,一开始我都是看代码看会的,很好理解,合并的代码如下

Code:

int merge(int a,int b)
{
    if((!a)||(!b))return a^b;
    down(a); 
    down(b);
    if(tree[a].rnd>tree[b].rnd){
        tree[a].r=merge(tree[a].r,b);
        change(a);  
        return a;
    }
    tree[b].l=merge(a,tree[b].l);
    change(b);
    return b;
}

可持久化Treap的其他操作

mergesplit操作可以拓展到别的操作。

注释:接下来的root都是整个序列对应的Treap根节点,也代表着整棵Treap

插入

对于插入操作,例如我要在root的第k个位置(即在前k个数之后)插入一个数x,那就先split(root,k),分成firstsecond两棵Treap,然后先合并firstx成为新的Treap,记为root,再合并rootsecond,得到的新的Treap即为完成了插入操作后的Treap

还是那句话,看看标程帮助理解。

Code:
#define fi first
#define se second
typedef pair<int,int> P;

int insert(int root,int x,int k)
{
    P ls=split(root,k);
    tree[++size]=x;
    tree[size].rnd=rand()%123456789;
    tree[size].size=1;
    root=merge(ls.fi,size);
    root=merge(root,ls.se);
    return root;
}
删除

对于删除操作,例如我要删除root的第k个位置上的数,那就先split(root,k),得到firstsecond两棵子树分别对应原序列的1~k位置上的数和k+1~size位置上的数,再对firstsplit操作,分离出1~k-1k,最后将1~k-1second两棵Treap合并便删除了第k个数。

看看代码。

Code:

#define fi first
#define se second
typedef pair<int,int> P;

int del(int root,int k)
{
    P ls=split(root,k);
    P s=split(ls.fi,k-1);
    root=merge(s.fi,ls.se);
    return root;
}

区间修改和询问

有了splitmerge操作,区间修改也不难。
利用split操作,将整棵Treap分成三棵Treap,分别为1~l-1l~rr+1~size,然后将中间的那段l~r区间对应的Treap打上一个标记,再从前往后合并三棵Treap即可。

询问也差不多是这样吧,都是分成三棵子树。

这个就没有必要写什么代码了,自己理解就好。

可持久化操作

仔细想一下,对于每次操作,都是由多个的splitmerge组成,然而对于每一个splitmerge操作,每一次改变的点数(信息发生变化的点的个数)期望为log n个,故当然可以持久化。

总结

相对于普通的Treap,可持久化Treap增加了可持久化操作和区间修改询问操作,却少了平衡树基本的旋转操作。
可持久化Treap大部分时候都可以取代splay,相比splay又更容易实现和调试,除了不能用于LCT,但陈bg大神说他找到用可持久化Treap维护LCT的方法,具体情况我也不是很清楚。

除此之外,可持久化Treap还能做到别的平衡树做不到的操作,也就是说有些题只能用可持久化Treap来做,这种题一般一定要用到split来完成一些奇怪的操作。

如果有什么错误或可以改进的地方,还请各位大佬指出,谢谢。

就这么多了,希望能够帮助到大家。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值