博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
Treap
阅读量:6710 次
发布时间:2019-06-25

本文共 5267 字,大约阅读时间需要 17 分钟。

Treap(平衡树)

注:本文有部分内容参照了清华大学计算机科学与技术系 02 班郭家宝 2010011267 论文,如与其他同学的博客有所雷同,纯属巧合。

首先引入几个基础概念

BST性质:

   一棵树中,左子树的值比当前的根小,右子树的值比当期的根大。左子树<根<右子树。

    1. 若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值;

    2. 若它的右子树不空,则右子树上所有结点的值均大于它根结点的值;

    3. 它的左、右子树也分别为二叉查找树。

   上述性质被称为 BST 性质。可以看出,二叉查找树是递归定义的。

 

如图, 是一个二叉查找树。

最小堆性质:

    最小堆,是一种经过排序的完全二叉树,其中任一非终端节点的数据值均不大于其左子节点和右子节点的值。

    通俗点讲:根节点的值比左右子树的值都要小。

 

       如图,为最小堆。

PS:

    之所以要讲BST性质,以及最小堆性质,因为Treap平衡树是他们结合起来的进化版。接下来就要进入正题了。

 

Treap的基本性质:Treap= Tree + Heap:

    Treap 是一种平衡树。它满足BST性质,即左孩子的值,小于根节点的值,右孩子的值大于根节点的值。

    Treap 在 BST 的基础上,添加了一个随机修正值。在满足 BST 性质的基础上,Treap 节点的修正值还满足最小堆性质

    最小堆性质可以被描述为每个子树根节点都小于等于其子节点。于是,Treap 可以定义为有以下性质的二叉树:

    1.  若它的左子树不空, 则左子树上所有结点的值均小于它的根结点的值, 而且它的根节点的修正值小于等于左子树根节点的修正值;

    2.  若它的右子树不空, 则右子树上所有结点的值均大它的根结点的值, 而且它的根节点的修正值小于等于右子树根节点的修正值;

    3.  它的左、右子树也分别为 Treap。

旋转操作:

    为了使Treap 中的节点同时满足BST 性质和最小堆性质,不可避免地要对其结构进行调整,当树的随机修正值不满足最小堆性质的时候,就要对树进行调整。

    这种调整方式被称为旋转。在维护Treap 的过程中,只有两种旋转,分别是左旋转(简称左旋)和右旋转(简称右旋)。

左旋转:把这个节点一下的树整体向左旋转,也就是把右节点旋到根的位置,而根的位置旋转到左结点的位置,如果右节点本身也有一个右节点,那么把它的右节点旋到根的右节点位置。

    至于这么放的原理,如果在大小关系上一时不能理解,那么可以认为,因为根的左结点已经有数了,所以只能把右节点的左结点放到根的右节点位置上。

    如果从BST性质的方向来看,这样子旋转是有利于维护BST性质的,因为根的右节点中的值都是比根节点的值要大的,所以旋转过来,即使是右节点的左结点,也会比根大,所以要放在根的右节点部位。

    可能越说越晕了,看图:

 

    如上图所示的左边的一个 Treap,它仍然满足BST 性质。但是由于某些原因,节点 4 和节点2 之间不满足最小堆序,4 作为2 的父节点,它的修正值大于左子节点的修正值。

    我们只有将2 变成4 的父节点,才能维护堆序。根据旋转的性质我们可以知道,由于2 是4 的左子节点,为了使 2 成为4 的父节点,我们需要把以 4 为根的子树右旋。右旋后,2 成为了4的父节点,满足堆序。

 

插入操作:

    在Treap 中插入元素,与在 BST 中插入方法相似。首先找到合适的插入位置,然后建立新的节点,存储元素。但是要注意建立新的节点的过程中,会随机地生成一个修正值,这个值可

能会破坏堆序,因此我们要根据需要进行恰当的旋转。具体方法如下:

    1. 从根节点开始插入;

    2. 如果要插入的值小于等于当前节点的值:

    在当前节点的左子树中插入,插入后如果左子节点的修正值小于当前节点的修正值,对当前节点进行右旋;

    3. 如果要插入的值大于当前节点的值:

    在当前节点的右子树中插入,插入后如果右子节点的修正值小于当前节点的修正值,对当前节点进行左旋;

    4.如果当前节点为空节点,在此建立新的节点,该节点的值为要插入的值,左右子树为空,插入成功。

删除操作:

    关于删除操作,我决定先从懒惰删除开始说起:

    懒惰删除就是在删除时,仅仅将元素找到后给元素打上“已被删除”的标记,而实际上不把

它从平衡树中删除。

    这种做法的优点是节约代码量,减少编程时间,对于初学者来说,是理解以及实现删除功能的好帮手。

但它的缺点也是很严重的:如果插入量和删除量都很大,这种删除方式会在平衡树中留下大量的“废节点”,浪费空间,还影响效率。

 

专业删除操作:

    情况一:该节点为叶节点或链节点,则该节点是可以直接删除的节点。若该节点有非空子节点,用非空子节点代替该节点的,否则用空节点代替该节点,然后删除该节点。

    情况二:该节点有两个非空子节点。我们的策略是通过旋转,使该节点变为可以直接删除的节点。如果该节点的左子节点的修正值小于右子节点的修正值,右旋该节点,使该节点降为右子树的根节点,然后访问右子树的根节点,继续讨论;反之,左旋该节点,使该节点降为左子树的根节点,然后访问左子树的根节点,继续讨论,直到变成可以直接删除的节点

代码实现如下(正确删除方式):

#include 
#include
#include
#include
#include
struct Tnode{ Tnode *son[2]; int fix, v; int siz; Tnode(int _v = 0) { son[0] = son[1] = 0; v = _v; } void Update() { siz = 1; if (son[0]) siz += son[0] -> siz; if (son[1]) siz += son[1] -> siz; }} *r, Mem[1000050], *cur;Tnode *NewNode(int v = 0){ (*cur) = Tnode(v); return cur ++;}void Rotate(Tnode *&x, int t){ Tnode *y = x -> son[t^1]; x -> son[t^1] = y -> son[t]; y -> son[t] = x; x -> Update(); y -> Update(); x = y;}void Insert(Tnode *&r, int x){ if (!r) { r = NewNode(x); r -> siz = 1; r -> fix = rand(); return ; } int t = (x > (r->v)); Insert(r -> son[t], x); if (r -> son[t] -> fix < r -> fix) Rotate(r, (t^1)); r -> Update();}int Ask(Tnode* r, int k){ if (!r) return 0; int leftsiz = (r->son[0]) ? (r -> son[0]->siz) : 0; if (k <= leftsiz) return Ask(r->son[0], k); if (k <= leftsiz + 1) return r -> v; return Ask(r -> son[1], k - leftsiz - 1);}void Delete(Tnode *&r, int x){ if (x == r -> v) { if (r -> son[0] && r -> son[1]) { int t = (r -> son[1] -> fix > r -> son[0] -> fix); Rotate(r, t); Delete(r -> son[t], x); r -> Update(); } else if (! r -> son[0] && ! r -> son[1]) { r = 0; } else if (r -> son[0]) r = r -> son[0]; else r = r -> son[1]; return ; } Delete(r -> son[x > r->v], x); r -> Update();}int main(){ freopen("2165.in", "r", stdin); freopen("2165.out", "w", stdout); srand((int)time(0)); cur = Mem; int N; scanf("%d", &N); r = 0; while (N--) { int t, x; scanf("%d%d", &t, &x); if (t == 1) { Insert(r, x); } else if (t == 2) { printf("%d\n", Ask(r, x)); } else { Delete(r, x); } } return 0;}

  

附录:

    关于Treap平衡树与其他平衡树的功能及速度比较。

关于treap的其他功能:

查找排名第k排名的元素:

    1.定义P 为当前访问的节点,从根节点开始访问,查找排名第 k 的元素;

    2.若满足P.left.size + 1<=k <= P.left.size + P.weight,则当前节点包含的元素就是排名第 k 的元素;

    3.若满足k <P.left.size + 1 ,则在左子树中查找排名第 k 的元素;

    4.若满足k >P.left.size + P.weight,则在右子树中查找排名第 k-(P.left.size +P.weight)的元素。

 

求元素的排名:

     在 Treap 中求元素的排名的方法与查找第 k 小的数是很相似的,可以近似认为是互为逆运算。

     1.定义P 为当前访问的节点,cur 为当前已知的比要求的元素小的元素个数。从根节点开始查找要求的元素,初始化 cur为0。

     2.若要求的元素等于当前节点元素,要求的元素的排名为区间[ P.left.size + cur + 1, P.left.size + cur + weight ]内任意整数;

     3.若要求的元素小于当前节点元素,在左子树中查找要求的元素的排名;

     4.若要求的元素大于当前节点元素,更新cur 为cur + P.left.size+weight ,在右子树中查找要求的元素的排名。

 

前驱与后继 :

    求一个元素在平衡树(或子树)中的前驱,定义为查找该元素在平衡树中不大于该元素的最大元素。相似的,求一个元素在平衡树(或子树)中的后继,定义为 查找该元素在平衡树中不小于该元素的最小元素。

求前驱:

    1. 从根节点开始访问,初始化最优节点为空节点;

     2. 如果当前节点的值不大于要求前驱的元素的值,更新最有节点为当前节点,访问当前节点的右子节点;

     3. 如果当前节点的值大于要求前驱的元素的值,访问当前节点的左子节点;

     4. 如果当前节点是空节点,查找结束,最优节点就是要求的前驱。

求后继:

     1. 从根节点开始访问,初始化最优节点为空节点;

     2. 如果当前节点的值不小于要求前驱的元素的值,更新最有节点为当前节点,访问当前节点的左子节点;

     3. 如果当前节点的值小于要求前驱的元素的值,访问当前节点的右子节点;

     4. 如果当前节点是空节点,查找结束,最优节点就是要求的后继。

转载于:https://www.cnblogs.com/yiyiyizqy/p/7396806.html

你可能感兴趣的文章
把oracle数据库恢复到某个时间点或者某个scn
查看>>
分组背包问题
查看>>
css的再深入4(更新中···)
查看>>
一道面试题
查看>>
大公司里怎样开发和部署前端代码?
查看>>
如何安装pycharm
查看>>
《Windows Internal》(2)
查看>>
数据监听进阶
查看>>
HTML5之Canvas绘图——图像切割函数clip
查看>>
五、箭头函数
查看>>
阿里Android开发规范:文件与数据库
查看>>
Android组件化专题 - 路由框架原理
查看>>
JQuery筛选器全系列介绍
查看>>
异步解决方案一:promise
查看>>
Clocksource tsc unstable
查看>>
两个sed小技巧:sed "/变量/变量/"
查看>>
ArrayAdapter的简单应用实例(初级入门引导)
查看>>
这大概是今年介绍云原生最清晰明了的文章!
查看>>
COGS314. [NOI2004] 郁闷的出纳员
查看>>
angular 7报错
查看>>