【学习】Treap平衡树

本文介绍了Treap数据结构,一种结合了二叉搜索树和堆性质的数据结构。Treap不仅实现简单,而且能基本实现随机平衡,具有O(logn)的时间复杂度。文章详细讲解了Treap的存储、旋转、插入、删除、查找等基本操作。

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

Treap简介

Treap 是指有一个随机附加域、满足堆的性质的二叉搜索树。其结构相当于以随机数据插入的二叉搜索树。其基本操作的期望时间复杂度为 O(logn) 。相对于其他的二叉搜索树,Treap 的特点是实现简单,且能基本实现随机平衡的结构。

Treap特性

Treap 在基本的二叉搜索树基础上增加了一个随机附加域,整棵树不仅要满足二叉搜索树左儿子小右儿子大的性质,同时也要满足按照随机附加域成一个堆。正因为附加域的随机性,使得 Treap 可以保持一个较为随机的结构,其平均树高为 O(logn) 级别,这 也是它能够实现基本操作时间复杂度 O(logn) 的原因。

Treap的存储

通常来说我们用一个结构体来存储一个Treap节点:

struct treap_node{
    int value, size, key;
    //value为点权值
    //key为随机附加域
    //size为子树的大小
    treap_node *ch[2];//记录左右儿子的指针
}

因为平衡树是一个动态的结构,所以节点需要动态的开。在C++里面,new操作符是比较慢的。所以建议大家不要使用。建议用内存池写法。
先估计一下,大约需要用到多少个Treap节点。

treap_node pool[max_node];
int tot = 0; 
//记录当前内存池中哪些节点已经使用

获取一个新节点:

inline treap_node *get_new(int value){
    treap_node *now = pool + ++top;
    now->value = value;
    now->size = 1;
    now->key = rand();
    now->ch[0] = now->ch[1] = null; 
    return now;
}

上面的代码里面出现了一个null,注意和空指针NULL区分。因为访问空指针会引起RE,为了避免麻烦,我们自己造一个假的空指针,这个指针null指向一个没有用的结构体。null的size=0,key=INF,这样根据堆的性质能保证它待在最底下而不会跑上来。

Treap的旋转

堆里面为了将内部元素调整成满足性质的,有下放和上放两个操作。分别是和儿子/父亲进行比较,如果不满足大小关系的限制就交换。
但是我们在平衡树中不能直接交换,直接交换就不满足二叉搜索树左儿子小右儿子大的性质。
我们如何既能实现树形态的改变(以满足堆的性质),又不影响其二叉搜索树的本质?
旋转旋转!

//取now是权值为3的节点
//wh为0
//(即左儿子旋转到now的位置)
void rotate(treap_node *&now, int wh){
    treap_node *child = now->ch[wh];
    now->ch[wh] = child->ch[wh ^ 1];
    child->ch[wh ^ 1] = now;
    now->update();
    child->update();
    now = child;
}

update操作是什么?
我们的Treap节点会维护一些和子树有关的信息,比如size(子树里面节点的个数)。当子树发生变化的时候,就需要更新这些信息:

void treap_node::update(){
    size=1+ch[0]->size+ch[1]->size;
}

在实际应用中,update函数还有可能维护一些别的信息,比如子树里面节点权值的最大者,节点权值和等等。

Treap的插入

如何找插入位置?

  • 从根节点开始找,如果新节点权值小于当前节点权值,去左子树;反之,去右子树。(我们这里先假设所有元素权值不相等)
  • 当我们到达一个叶子节点的时候,如果新点权值比叶子节点小,把新节点放左边;反之,放右边。

如何调整使其满足堆的性质?

  • 如果当前新节点的随机附加域的大小小于自己的父亲,我们就向上旋转。
void insert(treap_node *&now, int value) {
    if (now == null) {
        now = get_new(value);
        return;
    }
    if (now->value == value)
        return;
    int wh = value < now->value ? 0 : 1;
    insert(now->ch[wh], value);
    now->update();
    int minwh = 
        now->ch[0]->key < now->ch[1]->key ? 0 : 1;
    if (now->ch[minwh]->key < now->key)
        rotate(now, minwh);
}

Treap的删除

如果一个节点是叶子节点,我们能够很方便的删除。所以我们的思路是,把要删除的点挪到叶子节点的位置。一路旋转,注意还要维护堆的性质。
找两个儿子中key值较大的那个,和它进行旋转操作,直到自己变成叶子节点。

void del(treap_node *&now, int value) {
    if (now == null) return;
    if (now->value == value) {
        int minwh = 
            now->ch[0]->key < now->ch[1]->key ? 0 : 1;
        if (now->ch[minwh] != null) {
            rotate(now, minwh);
            del(now->ch[minwh ^ 1], value);
            now->update();
        }
        else
            now = null;
    }
    else {
        int wh = value < now->value ? 0 : 1;
        del(now->ch[wh], value);
        now->update();
    }
}

Treap的查找

和insert,del函数中的查找方式一样。值小向左走,值大向右走。

Treap计算Rank

计算一个数字当前是第几小(大)的
首先不停的查找这个数字。如果当前要向左走,答案不变;如果当前要向右走,答案要+左子树的大小+1。

int cou(treap_node *now, int value)
{
    if (now == null) return 0;
    int left_size = now->ch[0]->size;
    if (now->value == value) 
        return left_size;
    else if (value < now->value)
        return cou(now->ch[0], value);
    else
        return left_size + 1 + cou(now->ch[1], value);
}

Treap找第k大

这个过程和计算Rank正好相反。
看一下k和当前左子树大小的关系:
如果k<=ch[0]->size,往左走。
如果ch[0]->size+1==k,bingo
如果ch[0]->size+1

int kth(treap_node *now, int k)
{
    if (now == null) return INF;
    int left_size = now->ch[0]->size;
    if (k <= left_size)
        return kth(now->ch[0], k);
    else if (k == left_size + 1)
        return now->value;
    else
        return kth(now->ch[1], k - left_size - 1);
}

例题:SPOJ 3273 Order Statistic Set

题目大意:

你要实现一个动态集合,支持以下两种操作:
1.Insert(S,x):如果x不在集合S中,插入x。
2.Delete(S,x):如果x在集合S中,删除x。
并且支持以下两种查询操作:
1.K-th(k):返回集合S中第k小的元素。
2.Count(S,x):返回集合S中比x小的元素的个数。

输入:

第一行一个整数Q(Q<=200000)表示操作和查询的个数。
接下来每行,由一个大写字母和一个数字构成。I表示Insert,D表示Delete,K表示K-th,C表示Count。后面的数字即为操作中的x或者k。其中x绝对值小于1000000000,k为1到1000000000之间的整数。

输出:

对于每一个K-th操作,输出一行整数表示结果。若集合中元素不足k个,输出invalid。对于每一个Count操作,输出一行整数表示结果。

Treap总结

Treap相对于其它平衡树的优点是实现方便,且常数比较小。
缺点是功能不够强大。所以我们竞赛中通常采用splay而非treap。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值