平衡树

首先,我们回顾一下二叉查找树(binary search tree, BST)。

二叉查找树具有下列性质:

  1. 若左子树不空,则左子树上所有结点的值均小于它的根结点的值;
  2. 若右子树不空,则右子树上所有结点的值均大于它的根结点的值;
  3. 左、右子树也分别为二叉查找树;

BST 的插入

首先执行查找算法,找出被插结点的父亲结点。

若 key 值比当前结点小,则进入左儿子。

若 key 值比当前结点大,则进入右儿子。

//在二叉查找树中插入查找关键字key
void InsertBST(t,key) {
    if(t == NULL) {
        t = newBiTree;
        t->lchild = t->rchild=NULL;
        t->data = key;
        return;
    }
    if(key < t->data)
        InsertBST(t->lchild, key);
    else
        InsertBST(t->rchild, key);
}

最坏情况下,当先后插入的关键字有序时,二叉查找树会退化成链,树的深度为其平均查找长度 (n+1)/2?? 和(顺序查找相同),

最好的情况是二叉查找树的形态和折半查找的判定树相同,其平均查找长度和 logn 成正比。

优化:平衡树

常用的平衡二叉树有红黑树、AVL、Treap、Splay、SBT 等。

平衡二叉树的定义:它是一棵空树或它的左右两个子树的高度差满足一定限制以确保树的高度为 O(logn) 量级的

(如在 AVL 树中,确保左右子树高度的绝对值不超过 1),并且左右两个子树都是一棵平衡二叉树的二叉查找树。

树堆:Treap

相对于其他的平衡树,Treap 的特点是实现简单,且能基本实现随机平衡的结构。

Treap 是一棵二叉查找树,它的左子树和右子树分别是一个 Treap,

和一般的二叉查找树不同的是,Treap 记录额外的值——优先级。

Treap 以关键字构成二叉查找树的同时,优先级还需要满足堆的部分性质(结点的优先级大于该结点的孩子的优先级)。

注意二叉堆一定是完全二叉树,而 Treap 未必是。

Treap 的数据结构定义如下:

const int MAX_NODE = 10000;  // 最多 10000 个结点,根据题目的数据范围自行调整
struct treap {
    int v;  // 关键字
    int w;  // 同关键字的元素个数
    int rnd;  // 随机生成的优先级
    int size;  // 当前子树内的结点总数
    int l;  // 左孩子下标
    int r;  // 右孩子下标
} tr[MAX_NODE];

a) 旋转

旋转是基于随机产生的优先级来维护堆的性质,进而调整了 Treap 的形态。一共有两种旋转:左旋和右旋。

void rturn(int &k) {
    int t = tr[k].l;
    tr[k].l = tr[t].r;
    tr[t].r = k;
    k = t;
}

void lturn(int &k) {
    int t = tr[k].r;
    tr[k].r = tr[t].l;
    tr[t].l = k;
    k = t;
}

b) 插入

给结点随机分配一个优先级,先把要插入的点插入到一个叶子上,然后跟维护堆一样,自底向上调整。

若当前结点的优先级比根大就旋转,其中,若它是父亲的左儿子就右转,若它是父亲的右儿子就左转。

最多进行 h 次(h 是树的高度)旋转,插入的复杂度是 O(logn) 的。在期望意义下,单次操作的复杂度是 O(logn)。

void insert(int &k, int x) {
    if (k == 0) {
        size++;
        k = size;
        tr[k].size = tr[k].w = 1;
        tr[k].v = x;
        tr[k].rnd = rand();
        return;
    }
    tr[k].size++;
    if (tr[k].v == x)
        tr[k].w++;  // 每个结点顺便记录下与该结点相同值的数的个数
    else if (x > tr[k].v) {
        insert(tr[k].r, x);
        if (tr[tr[k].r].rnd < tr[k].rnd)
            lturn(k);  // 维护堆性质
    } else {
        insert(tr[k].l, x);
        if (tr[tr[k].l].rnd < tr[k].rnd)
            rturn(k);  // 维护堆性质
    }
}

在使用 Treap 时,应该在外面通过如下的方法进行初始化和插入操作:

int root = 0;
insert(root, 1);  // 这次插入后,root 被修改为 1
insert(root, 2);  // 之后 root 都是 1
insert(root, 3);  // 不过每次 insert 时的代码都一样,用起来没有任何区别

c) 删除

只需要该元素自顶向下调整,与优先级较大的儿子交换,直到叶子结点,直接删除。这样,该树仍然保持树堆的性质。在

期望意义下,删除最多进行 O(logn) 次旋转,时间复杂度依然是 O(logn) 的。

void del(int &k, int x) {  // 删除关键字 x
    if (k == 0)
        return;
    if (tr[k].v == x) {
        if (tr[k].w > 1) {  // 若关键字 x 有多个,只删去一个
            tr[k].w--;
            tr[k].size--;
            return;
        }
        if (tr[k].l * tr[k].r == 0)  // 有一个儿子为空
            k = tr[k].l + tr[k].r;
        else if (tr[tr[k].l].rnd < tr[tr[k].r].rnd)
            rturn(k), del(k, x);
        else
            lturn(k), del(k, x);
    }
    else if(x > tr[k].v)
        tr[k].size--, del(tr[k].r , x);
    else
        tr[k].size--, del(tr[k].l, x);
}
del(root, 3);  // 删除结点值为 3 的元素

d) 查询

和一般的二叉查找树一样,但是由于 Treap 的随机化结构,可以证明 Treap 中查找的期望复杂度是 O(logn)。

另外,基于上述基本操作,我们还可以支持查询指定关键字的排名、查询第 k 大、查询前驱后继等操作,时间复杂度均为 O(logn)。

时间: 2024-08-24 01:23:13

平衡树的相关文章

bzoj3224 Tyvj 1728 普通平衡树

Description 您需要写一种数据结构(可参考题目标题),来维护一些数,其中需要提供以下操作: 1. 插入x数 2. 删除x数(若有多个相同的数,因只删除一个) 3. 查询x数的排名(若有多个相同的数,因输出最小的排名) 4. 查询排名为x的数 5. 求x的前驱(前驱定义为小于x,且最大的数) 6. 求x的后继(后继定义为大于x,且最小的数) Input 第一行为n,表示操作的个数,下面n行每行有两个数opt和x,opt表示操作的序号(1<=opt<=6) Output 对于操作3,4,

bzoj 3224: Tyvj 1728 普通平衡树.

3224: Tyvj 1728 普通平衡树 Time Limit: 10 Sec  Memory Limit: 128 MBSubmit: 15114  Solved: 6570[Submit][Status][Discuss] Description 您需要写一种数据结构(可参考题目标题),来维护一些数,其中需要提供以下操作:1. 插入x数2. 删除x数(若有多个相同的数,因只删除一个)3. 查询x数的排名(若有多个相同的数,因输出最小的排名)4. 查询排名为x的数5. 求x的前驱(前驱定义为

数组splay ------ luogu P3369 【模板】普通平衡树(Treap/SBT)

二次联通门 : luogu P3369 [模板]普通平衡树(Treap/SBT) #include <cstdio> #define Max 100005 #define Inline __attri\ bute__( ( optimize( "-O2" ) ) ) Inline void read (int &now) { now = 0; register char word = getchar (); bool temp = false; while (wor

对于各种各样平衡树的比较

近来闲来无事...难题不会做,简单题不想做... 又不能颓废,于是就去学各种各样的平衡树 故在此对各种平衡树做一些比较(不太常见的, Treap这样烂大街的就不比了) 二次联通门 : 数组splay ------ luogu P3369 [模板]普通平衡树(Treap/SBT) 二次联通门 : 替罪羊树 ------ luogu P3369 [模板]普通平衡树(Treap/SBT) 二次联通门 : 红黑树 ------ luogu P3369 [模板]普通平衡树(Treap/SBT) 评测地址为

平衡树初阶——AVL平衡二叉查找树+三大平衡树(Treap + Splay + SBT)模板【超详解】

平衡树初阶——AVL平衡二叉查找树 一.什么是二叉树 1. 什么是树. 计算机科学里面的树本质是一个树状图.树首先是一个有向无环图,由根节点指向子结点.但是不严格的说,我们也研究无向树.所谓无向树就是将有向树的所有边看成无向边形成的树状图.树是一种递归的数据结构,所以我们研究树也是按照递归的方式去研究的. 2.什么是二叉树. 我们给出二叉树的递归定义如下: (1)空树是一个二叉树. (2)单个节点是一个二叉树. (3)如果一棵树中,以它的左右子节点为根形成的子树都是二叉树,那么这棵树本身也是二叉

【bzoj3224】Tyvj 1728 普通平衡树 平衡树的三种姿势 :splay,Treap,ScapeGoat_Tree

直接上代码 正所谓 人傻自带大常数 平衡树的几种姿势:  AVL Red&Black_Tree 码量爆炸,不常用:SBT 出于各种原因,不常用. 常用: Treap 旋转 基于旋转操作和随机数堆 但不支持区间操作. 非旋转 基于随机数堆和拆分合并操作 常数较大 Spaly 完全基于旋转 各种操作 ScapeGoat_Tree 基于a权值平衡树和压扁重构 无旋转 但不支持区间操作 PS:非旋转可以实现平衡树的可持久化,从而来套一些东西 splay #include<cstdio> #de

Tyvj 1729 文艺平衡树

Time Limit: 10 Sec  Memory Limit: 128 MBSubmit: 4604  Solved: 2691[Submit][Status][Discuss] Description 您需要写一种数据结构(可参考题目标题),来维护一个有序数列,其中需要提供以下操作:翻转一个区间,例如原有序序列是5 4 3 2 1,翻转区间是[2,4]的话,结果是5 2 3 4 1 Input 第一行为n,m n表示初始序列有n个数,这个序列依次是(1,2……n-1,n)  m表示翻转操作

BZOJ3196 二逼平衡树

3196: Tyvj 1730 二逼平衡树 Time Limit: 10 Sec  Memory Limit: 128 MB Description 您需要写一种数据结构(可参考题目标题),来维护一个有序数列,其中需要提供以下操作:1.查询k在区间内的排名2.查询区间内排名为k的值3.修改某一位值上的数值4.查询k在区间内的前驱(前驱定义为小于x,且最大的数)5.查询k在区间内的后继(后继定义为大于x,且最小的数) Input 第一行两个数 n,m 表示长度为n的有序序列和m个操作第二行有n个数

BZOJ1588: [HNOI2002]营业额统计(正解:平衡树,我的歪解:暴力)

Description 营业额统计 Tiger最近被公司升任为营业部经理,他上任后接受公司交给的第一项任务便是统计并分析公司成立以来的营业情况. Tiger拿出了公司的账本,账本上记录了公司成立以来每天的营业额.分析营业情况是一项相当复杂的工作.由于节假日,大减价或者是其他情况的时候,营业额会出现一定的波动,当然一定的波动是能够接受的,但是在某些时候营业额突变得很高或是很低,这就证明公司此时的经营状况出现了问题.经济管理学上定义了一种最小波动值来衡量这种情况: 该天的最小波动值 当最小波动值越大