treap随想

介绍了一种新的平衡二叉搜索树数据结构,通过旋转维护左右子树深度差不超过1来保持平衡,提供了代码实现及理论证明,展示了该树在随机数据集下表现良好。

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

(希望各位大佬不要怒对我,我只是想跟大家讨论讨论。)

刚才YY了出了一种数据结构,也不知道叫什么名字(我的意思是,估计以前已经有很多人YY出来过并且已经命了名了),也不知道时间复杂度是否正确(毕竟这是YY出来的),如果各位大神发现了本文证明中的问题,欢迎在下方评论区评论。

定义

这种数据结构是一种二叉搜索树,或者说是一种平衡树。

lch(x)lch(x) 表示xx结点的左子,rch(x)表示xx结点的右子。如果一个结点没有左子,则lch(x)=null。同理,如果一个结点没有右子,则rch(x)=nullrch(x)=null。另外,规定lch(null)=rch(null)=nulllch(null)=rch(null)=null

len(x)len(x)表示以xx为根的子树中深度最大的叶子结点的深度,定义式为len(x)=1+max{len(lch(x),len(rch(x)))}。规定len(null)=0len(null)=0

旋转同treap/splay。左旋表示把原来的根节点变为新根节点的左子,右旋表示把原来的根节点变为新根节点的右子(对不起,我插入不了图片)。

操作

操作很简单,就是每当|len(lch(x))len(rch(x))|>1|len(lch(x))−len(rch(x))|>1时,把xx旋入len较小的子树。

一个随性的代码

#include <cstdio>
#include <cmath>
#include <cstdlib>
#include <ctime>
#include <algorithm>
using namespace std;

const int maxn = 2*1000000 + 6;
namespace BST {
    int ch[maxn][2], val[maxn], siz[maxn], len[maxn], ncnt;
    void maintain(int rt) {
        siz[rt] = 1 + siz[ch[rt][0]] + siz[ch[rt][1]];
        len[rt] = 1 + max(len[ch[rt][0]], len[ch[rt][1]]);
    }
    int shortson(int rt) { /// 返回较 "短"的儿子 
        return len[ch[rt][0]]<len[ch[rt][1]] ? 0: 1;
    }
    void rotate(int& x, int d) {
        int k = ch[x][d^1]; ch[x][d^1] = ch[k][d];
        ch[k][d] = x; maintain(x); x = k; maintain(x);
    }
    int dir(int t, int v) {
        return v<val[t] ? 0:1;
    }
    void insert(int& t, int v) {
        if(t == 0) {
            t = ++ ncnt; ch[t][0] = ch[t][1] = 0;
            val[t] = v; siz[t] = 1; len[t] = 1; return;
        }
        int d = dir(t, v); insert(ch[t][d], v); maintain(t);
        if(abs(len[ch[t][0]] - len[ch[t][1]]) > 1)
            rotate(t, shortson(t)); /// 维护最长链 
    }
    int rnk(int t, int v) {
        if(t == 0) return 1;
        int lsiz = 1 + siz[ch[t][0]];
        int d = dir(t, v);
        if(d == 0) return rnk(ch[t][0], v);
        else       return rnk(ch[t][1], v) + lsiz;
    }
    #define inf (0x7f7f7f7f)
    int kth(int t, int k) {
        if(t == 0) return k<=0? -inf:inf;
        int lsiz = 1 + siz[ch[t][0]];
        if(k <= lsiz) return kth(ch[t][0], k);
        else          return kth(ch[t][1], k - lsiz);
    }
    void del(int& t, int x) {
        if(t == 0) return;
        if(val[t] != x) {
            int d = dir(t, x); del(ch[t][d], x);
            maintain(t);
            if(abs(len[ch[t][0]] - len[ch[t][1]]) > 1)
                rotate(t, shortson(t)); /// 维护最长链 
        }else {
            if(ch[t][0]==0 || ch[t][1]==0) {
                t = ch[t][0] + ch[t][1]; /// !不能 maintain(0) 
            }else {
                rotate(t, 0); del(ch[t][0], x); /// 在左子中删除 
                maintain(t);
                if(abs(len[ch[t][0]] - len[ch[t][1]]) > 1)
                    rotate(t, shortson(t)); /// 维护最长链 
            }
        }
    }
    void debug() {
        for(int i = 1; i <= ncnt; i ++) {
            printf("%3d: %3d, %3d, %3d\n", i, ch[i][0], ch[i][1], val[i]);
        }
    }
}

这还有一份:代码 https://paste.ubuntu.com/p/PHFnVZ4Rq4/

证明

我们假定这棵树的任意一个结点x,在插入操作结束时都能保证|len(lch(x))len(rch(x))|1|len(lch(x))−len(rch(x))|≤1。(如果您感兴趣可以试着去证明一发,不过我把这个当成结论。如果有人证明出这是错的,请在评论区评论。)

定义f(H)f(H)表示数高为H的树拥有的最少的结点数,那么有f(H)=1+f(H1)+f(H2)f(H)=1+f(H−1)+f(H−2)。理由是这棵树有根节点,除去根节点之后的两棵子树的高度为H-1。又因为两棵树可以有1的高度差,所以可以有一棵子树的高度为H-2。

根据计算可以得出:

f[  2]=         3
f[  3]=         5
f[  4]=         9
f[  5]=        15
f[  6]=        25
f[  7]=        41
f[  8]=        67
f[  9]=       109
f[ 10]=       177
f[ 11]=       287
f[ 12]=       465
f[ 13]=       753
f[ 14]=      1219
f[ 15]=      1973
f[ 16]=      3193
f[ 17]=      5167
f[ 18]=      8361
f[ 19]=     13529
f[ 20]=     21891
f[ 21]=     35421
f[ 22]=     57313
f[ 23]=     92735
f[ 24]=    150049
f[ 25]=    242785
f[ 26]=    392835
f[ 27]=    635621
f[ 28]=   1028457
f[ 29]=   1664079
f[ 30]=   2692537

f[ 28]= 1028457 所以,一棵1000000的树的深度不会超过28(随机数据实测深度为25)。

求了一下通项公式,不知道对不对,求各位大佬指导。

fn=(1+15)(1+52)n+(115)(152)n1fn=(1+15)⋅(1+52)n+(1−15)⋅(1−52)n−1

我的校验程序:

#include <cstdio>
#include <cmath>
#include <algorithm>
using namespace std;
#define long long long

const int maxn = 100 + 3;
long f[maxn], g[maxn];

double d(double i) {
    double sqrt5 = sqrt(5.0);
    double x0 = (1.0 + sqrt5)/2.0;
    double x1 = (1.0 - sqrt5)/2.0;
    double A = (sqrt5 - 1.0)/2.0;
    double B = (-1.0 - sqrt5)/2.0;
    double ans = A*pow(x0, i) + B*pow(x1, i);
    return ans;
}

double Sd(int n) {
    double sqrt5 = sqrt(5.0);
    double x0 = (1.0 + sqrt5)/2.0;
    double x1 = (1.0 - sqrt5)/2.0;
    double A = (sqrt5 - 1.0)/2.0;
    double B = (-1.0 - sqrt5)/2.0;
    double ans = A*x0*(1.0-pow(x0,n))/(1.0-x0) + B*x1*(1.0-pow(x1,n))/(1.0-x1);
    return ans;
}

double Ans(int n) {
    double sqrt5 = sqrt(5);
    double x0 = (1.0 + sqrt5)/2.0;
    double x1 = (1.0 - sqrt5)/2.0;
    double A = 1.0 + 1.0/sqrt5;
    double B = 1.0 - 1.0/sqrt5;
    double ans = A*pow(x0, n) + B*pow(x1, n) -1;
    return ans;
}

int main() {
    for(int i = -1; i <= 10; i ++) {
        printf("%3d : %lf\n", i, Sd(i));
    }
    f[0] = 1; f[1] = 1; g[0] = 0; g[1] = 1;
    for(int i = 2; i <= 30; i ++) {
        f[i] = 1 + f[i-1] + f[i-2];
        g[i] = g[i-1] + g[i-2];
        printf("f[%3d] %10lld %20lf\n", i, f[i], Ans(i));
    }
    return 0;
}

运行结果:

 -1 : 1.000000
  0 : 0.000000
  1 : 2.000000
  2 : 3.000000
  3 : 6.000000
  4 : 10.000000
  5 : 17.000000
  6 : 28.000000
  7 : 46.000000
  8 : 75.000000
  9 : 122.000000
 10 : 198.000000
f[  2]          3             3.000000
f[  3]          5             5.000000
f[  4]          9             9.000000
f[  5]         15            15.000000
f[  6]         25            25.000000
f[  7]         41            41.000000
f[  8]         67            67.000000
f[  9]        109           109.000000
f[ 10]        177           177.000000
f[ 11]        287           287.000000
f[ 12]        465           465.000000
f[ 13]        753           753.000000
f[ 14]       1219          1219.000000
f[ 15]       1973          1973.000000
f[ 16]       3193          3193.000000
f[ 17]       5167          5167.000000
f[ 18]       8361          8361.000000
f[ 19]      13529         13529.000000
f[ 20]      21891         21891.000000
f[ 21]      35421         35421.000000
f[ 22]      57313         57313.000000
f[ 23]      92735         92735.000000
f[ 24]     150049        150049.000000
f[ 25]     242785        242785.000000
f[ 26]     392835        392835.000000
f[ 27]     635621        635621.000000
f[ 28]    1028457       1028457.000000
f[ 29]    1664079       1664079.000000
f[ 30]    2692537       2692537.000000
Treap(Tree + Heap)是一种结合了二叉搜索树(BST)和堆(Heap)特性的平衡二叉搜索树数据结构。其核心原理在于每个节点维护两个值:一个用于二叉搜索树性质的键值(key),以及一个用于最大堆性质的优先级(priority)。通过这两个属性,Treap在插入和删除操作时保持树的平衡性,从而确保操作的时间复杂度接近于对数级别。 ### Treap的基本原理 1. **二叉搜索树性质**:对于任意节点,其左子树中所有节点的键值小于当前节点的键值,右子树中所有节点的键值大于当前节点的键值。 2. **堆性质**:每个节点的优先级大于其子节点的优先级,这样可以确保树的结构在插入或删除时通过旋转操作保持平衡。 在插入新节点时,Treap首先按照二叉搜索树的方式找到合适的位置,并赋予该节点一个随机的优先级。如果该节点的优先级违反了堆性质,则通过旋转操作调整树的结构以恢复堆性质。删除操作类似,通过旋转确保堆性质得以维持。 ### 无旋Treap的实现 无旋Treap(Non-Rotating Treap)是Treap的一个变种,它通过分裂(Split)和合并(Merge)操作来实现树的平衡,而不是传统的旋转操作。分裂操作将树按照某个键值或位置分割为两部分,而合并操作将两个树合并为一个。这种方式简化了实现逻辑,尤其是在处理复杂操作时[^2]。 例如,分裂操作可以通过以下方式实现(以键值为分割点): ```python def split(node, key): if node is None: return (None, None) if node.key <= key: left, right = split(node.right, key) node.right = left return (node, right) else: left, right = split(node.left, key) node.left = right return (left, node) ``` 合并操作则需要确保堆性质的维护: ```python def merge(left, right): if left is None: return right if right is None: return left if left.priority > right.priority: left.right = merge(left.right, right) return left else: right.left = merge(left, right.left) return right ``` ### 应用场景 Treap由于其高效的平衡特性,广泛应用于需要高效查找、插入和删除操作的场景。常见的应用包括: - **数据库索引**:Treap可以用于实现高效的索引结构,支持快速的数据检索。 - **内存中的集合与映射**:在需要频繁插入和删除元素的场景中,Treap提供了良好的性能保障。 - **算法竞赛**:在某些需要高效数据结构的竞赛题中,Treap常被用来实现动态集合操作[^1]。 ### 实现细节 在实现Treap时,需要注意以下几点: - **随机优先级生成**:为了保证树的平衡性,每个节点的优先级应随机生成,通常使用大范围的整数以减少冲突的可能性。 - **递归与非递归实现**:虽然递归实现较为直观,但在大规模数据处理时可能会导致栈溢出,因此在实际应用中可以考虑非递归实现。 - **类型安全与内存管理**:特别是在使用Rust等语言时,需注意类型约束和内存安全,以确保程序的稳定性和可靠性[^1]。 通过理解Treap的原理和实现方式,开发者可以更好地将其应用于实际项目中,并根据需求进行扩展和优化。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值