入门平衡树:\(treap\)

前言:

  • 如有任何错误和其他问题,请联系我

    • 微信/QQ同号:615863087

前置知识:

  • 二叉树基础知识,即简单的图论知识。

初识\(BST\):

  • \(BST\)是\((Binary\:\:Search\:\:Tree)\)的简写,中文名二叉搜索树。
  • 想要了解平衡树,我们就先要了解这样一个基础的数据结构: 二叉搜索树。
  • 所以接下来会先大篇幅讨论\(BST\)
  • 了解了\(BST\)后,\(Treap\)也就顺理成章了。
  • 二叉树有两类非常重要的性质:
    • 1:堆性质

      • 堆性质又分为大根堆性质和小根堆性质。小根堆的根节点小于左右孩子,且这是一个递归定义。对于大根堆则是大于。\(c++\)提供的\(priorioty\_queue\)就是一个大根堆。
    • 2:\(BST\)性质
      • 给定一棵二叉树,树上的每个节点都带有一个数值,成为节点的“关键吗”。对于树中任意一个节点满足\(BST\)性质,指:

        • 该节点的关键码不小于他左子树的任意节点的关键码。
        • 该节点的关键码不大于他右子树的任意节点的关键码。
  • 举个拓展的例子,笛卡尔树既满足堆性质,又满足\(BST\)性质。
    • 笛卡尔树并不常见,博主也只见过几次,也偶尔成功地用别的数据结构\(AC\)掉对应的题目,是下来后看题解才发现可以用笛卡尔树写。

    • 似乎可以用单调栈替代?希望了解的奆奆可以联系我或者评论区告诉我。

    • 这里给出一张笛卡尔树的图片帮助大家了解\(BST\)性质和堆性质,不多做介绍。

    • (图源:维基百科)

    • 笛卡尔树中的\(index\)满足\(BST\)性质,\(value\)满足堆性质。

      • 其中\(index\)是图中蓝色框出现的顺序,\(val\)是蓝色框出现的权值。
      • 很显然这棵树满足小根堆性质,也满足\(BST\)性质。
  • 那么我们接下来介绍的二叉搜索树就是一棵满足\(BST\)性质的二叉树,可以结合此图加深理解。

\(BST\)的相关操作:

\(BST\)的建立:
  • 为了避免数组越界同时减少特判,我们一般在\(BST\)中插入两个额外的节点,其中两个节点的权值其中一个为\(+INF\),另一个为\(-INF\)。
  • 不明白为什么能够减少特判可以先往下阅读。
  1. const int INF = 0x3f3f3f3f;
  2. int tot, root, n;
  3. struct Treap //入门级平衡树
  4. {
  5. int l, r; //左右子节点在数组中的下标
  6. int val; //节点权值
  7. }a[maxn];
  8. int New(int val)
  9. {
  10. a[++tot].val = val;
  11. return tot;
  12. }
  13. void Build()
  14. {
  15. New(-INF), New(INF);
  16. root = 1; a[1].r = 2;
  17. }
\(BST\)的检索:
  • 为了方便,我们假设树中没有相同权值的节点。
  • 在\(BST\)中检索关键码为\(val\)的节点。根据\(BST\)性质,我们可以:
    • 当\(val\)等于当前节点的关键码

      • 说明找到了这个节点
    • 当\(val\)小于当前节点的关键码
      • 若当前节点左子树为空,则说明不存在\(val\)。
      • 否则递归进入左子树。
    • 当\(val\)大于当前节点的关键码
      • 若当前节点右子树为空,则说明不存在\(val\)。
      • 否则递归进入右子树
  1. int Get(int &p, int val)
  2. {
  3. if(p == 0)
  4. {
  5. p = New(val);
  6. return;
  7. }
  8. if(val == a[p].val) return p;
  9. return val < a[p].val ? Get(a[p].l, val) : Get(a[p].r, val);
  10. }
\(BST\)的插入:
  • 为了方便,我们假设即将插入的点的权值在树中不存在。
  • 与检索过程类似,当发现要走向的子节点为空的时候,直接建立关键码为\(val\)的新节点为\(p\)的子节点。
  1. void ins(int &p, int val)
  2. {
  3. if(p == 0)
  4. {
  5. p = New(val);
  6. return;
  7. }
  8. if(val == a[p].val) return;
  9. if(val < a[p].val) ins(a[p].l, val);
  10. else ins(a[p].r, val);
  11. }
\(BST\)求前驱/后继:
  • \(val\)的前驱指小于\(val\)且最大的数,\(val\)的后继指大于\(val\)且最小的数。

  • 求后继:

    • 初始化\(ans\)为具有正无穷关键码的那个节点的编号。然后在\(BST\)中检索\(val\)。在检索过程中,每经过一个节点,都检查节点的关键码,判断能否更新所求的后继。
    • 检索完成有三种可能:
    • \(1:\) 没有找到\(val\),此时\(val\)的后继就在已经经过的节点中。此时\(ans\)为所求。
    • \(2:\) 找到了关键码为\(val\)的节点\(p\),但\(p\)没有右子树。那么此时的\(ans\)为所求。
    • \(3:\) 找到了关键码为\(val\)的节点\(p\),且\(p\)有右子树,那么就从\(p\)的右孩子出发一直向左走,就找到了\(val\)的后继。
    1. int GetNext(int val)
    2. {
    3. int ans = 2; // a[2].val = INF
    4. int p = root;
    5. while(p)
    6. {
    7. if(val == a[p].val)
    8. {
    9. if(a[p].r > 0)
    10. {
    11. p = a[p].r;
    12. while(a[p].l > 0) p = a[p].l;
    13. ans = p;
    14. }
    15. break;
    16. }
    17. if(a[p].val >val && a[p].val < a[ans].val) ans = p;
    18. p = val < a[p].val ? a[p].l : a[p].r;
    19. }
    20. return a[ans].val;
    21. }
\(BST\)的节点删除:
  • 从\(BST\)中删除节点权值为\(val\)的数字。
  • 首先检索得到权值为\(val\)的节点\(p\)。
  • 节点\(p\)的子节点数目小于\(2\),则直接删除该节点,并让子节点代替\(p\)的位置。
  • 若\(p\)节点又有左子树又有右子树,则求出\(p\)的后继节点\(next\)。根据我们上面的分析,后继是进入右子树后一直往左走,那么这个后继节点\(next\)是不会有左子树的。这一点可以用反证法简单的证明一下。
    • 如果此时的\(next\)节点有左子树,此时再来看后继的定义:大于\(val\)的最小数,那么此时\(next\)再往左走能得到大于\(val\)的且比\(next\)节点权值更小的一个数,即\(next\)不为\(p\)的后继节点。矛盾。
  • 此时我们直接删除节点\(next\)(删除之前要记录一下),再用\(next\)的右子树代替节点\(next\)的位置,再删除\(p\),再用记录下来的\(next\)代替\(p\)的位置。
其他:
  • 来解决一下为什么插入两个特殊的节点能减少特判的原因。

  • 在主观上,我们去画图模拟这样一棵\(BST\),有没有这两个节点是无所谓的。但是到了代码实现里,插入这两个节点却极大的方便了我们代码的编写。

  • 手动模拟插入几个节点试试看会是什么样:

  • 这样的话十分清楚了,如果我们插入的时候还没有节点我们也可以直接插入,检索前驱后继也可以分别从一号或者二号节点开始,即使删除操作把点全部删光了依然会有两个节点在那。我们每次操作只需要关注我们想做的事,不需要关注什么乱七八糟的有没有节点啊,树是不是空的啊,查找前驱我应该从哪里开始啊等等等等的问题,就能完完全全根据伪代码写。(话糙理不糙)【划掉。

\(BST\)复杂度分析
\(BST\)时间复杂度分析:
  • 一棵\(N\)个节点的二叉树深度一般为\(logN\),所以他也能保证每一次操作的复杂度为\(O(logN)\)。
  • 但是当遇到插入一个有序序列的情况,那么\(BST\)将退化成一条链,操作的复杂度也将退化为\(O(n)\)。
  • 为了解决这个问题,保证\(BST\)的平衡,产生了各种平衡树如\(AVL\)树,红黑树,替罪羊树等。
    • (这三个除了红黑树以外我都没了解过\(QwQ\))
  • 其中入门级别的平衡树是\(Treap\),较为常用的是\(Splay\)。本着入门的精神,接下来我们来看看\(Treap\)。

\(Treap\)

旋转操作:
  • 对于一个序列\(1,2,3,4,5\),或者乱序成\(2,3,1,5,4\)。将它们插入\(BST\)中,得到的\(BST\)其实是等价的。但是第一种显然在复杂度上不占优。

  • 我们将通过变形来更改\(BST\)的形态但同时让他保持原来的信息这样的操作,叫做"旋转"。旋转又分为左旋和右旋。

  • 可以结合此图加深理解

  • 即我们可以通过旋转来将原本退化的\(BST\)通过等价变换成为一棵能保证复杂度的树。

什么样旋转是合理的?
  • 在随机数据下,一棵\(BST\)就是平衡的。所以\(treap\)的思想就是利用随机来创造平衡条件。
  • \(treap\)是\(tree\)和\(heap\)的结合。所以\(treap\)有两个关键字,一个是随机附上的数值,一个是原本的权值。
  • 当我们插入一个新结点的时候,我们对其附上一个随机值,然后像二叉堆那样进行左旋或者右旋来进行调整,这样既能保证平衡性又能保证\(BST\)性质。
    • 好像有点靠脸\(AC\)的意思?【划掉
    • 但只要没有被大佬\(\%\)而导致\(rp-=INF\)问题应该也不大。
    • \(Splay\)是一个不错的选择,更通用,也不会因为随机建值靠脸拿分。

模板题:

Acwing255: 普通平衡树

洛谷3369: 普通平衡树

  • 两个地址提供的是一道题,但建议两个地方都交一下。

  • 代码模板源于蓝书,详细注释

  1. #include<bits/stdc++.h>
  2. using namespace std;
  3. typedef long long ll;
  4. const int maxn = 1e5 + 10;
  5. const int INF = 0x3f3f3f3f;
  6. int tot, root, n;
  7. struct Treap
  8. {
  9. int l, r; //左右子节点
  10. int val, dat; //Bst的权值 堆的权值 满足大根堆性质
  11. int cnt, sz; //重复的数量 子树(包括根节点)的大小
  12. }a[maxn];
  13. //新建一个节点
  14. int New(int val)
  15. {
  16. a[++tot].val = val;
  17. a[tot].dat = rand(); //给堆的那一部分附上随机值
  18. a[tot].cnt = a[tot].sz = 1; //此时重复数和子树都为1
  19. return tot;
  20. }
  21. void update(int p){ //更新当前父节点的信息
  22. a[p].sz = a[a[p].l].sz + a[a[p].r].sz + a[p].cnt;
  23. }
  24. //初始建立treap 其中有两个特殊节点
  25. void Build()
  26. {
  27. New(-INF), New(INF);
  28. root = 1; a[1].r = 2;
  29. update(root); //更新根节点
  30. }
  31. int GetRankByVal(int p, int val)
  32. {
  33. if(p == 0) return 0; //如果没有这个节点
  34. //如果此时找到了 那么排名就为小于他的数字+自身
  35. if(val == a[p].val) return a[a[p].l].sz + 1;
  36. //可以直接向左走
  37. if(val < a[p].val) return GetRankByVal(a[p].l, val);
  38. //向右走的话其实就相当于左半部分加上根全部小于他
  39. return GetRankByVal(a[p].r, val) + a[a[p].l].sz + a[p].cnt;
  40. }
  41. //查找排名为rk的数
  42. int GetValByRank(int p, int rk)
  43. {
  44. if(p == 0) return INF; //空节点
  45. //如果当前节点左子树的规模大于rk
  46. //那么说明答案一定在左子树中
  47. if(a[a[p].l].sz >= rk)
  48. return GetValByRank(a[p].l, rk);
  49. if(a[a[p].l].sz + a[p].cnt >= rk)
  50. return a[p].val;
  51. //向右走的时候rk要减去左子树和根节点的规模
  52. return GetValByRank(a[p].r, rk - a[a[p].l].sz - a[p].cnt);
  53. }
  54. void zig(int &p) //右旋操作
  55. {
  56. int q = a[p].l; //左子树
  57. a[p].l = a[q].r; //左子树的右子树拼接到根节点的左子树上
  58. a[q].r = p; //左子树的右子树变为根节点 左子树的左子树不变
  59. p = q; //把原来左子树的信息换过去
  60. update(a[p].r); update(p);//例行更新两个父节点的信息
  61. }
  62. void zag(int &p) //左旋操作 同理与右旋
  63. {
  64. int q = a[p].r;
  65. a[p].r = a[q].l; a[q].l = p; p = q;
  66. update(a[p].l); update(p);
  67. }
  68. //插入和删除操作 一般会涉及到旋转
  69. //所以这时候采用递归写法 以便于更新节点信息
  70. void ins(int &p, int val)
  71. {
  72. if(p == 0)
  73. {
  74. p = New(val);
  75. return;
  76. }
  77. if(val == a[p].val)
  78. {
  79. a[p].cnt++; update(p);
  80. return;
  81. }
  82. if(val < a[p].val)
  83. {
  84. ins(a[p].l, val); //因为需要满足大根堆性质
  85. //当我的左子节点的堆权值大于根节点的时候
  86. //右旋把左子节点换上来
  87. if(a[p].dat < a[a[p].l].dat) zig(p);
  88. }
  89. else
  90. {
  91. ins(a[p].r, val);
  92. if(a[p].dat < a[a[p].r].dat) zag(p);
  93. }
  94. update(p);
  95. }
  96. //找到需要删除的节点并将其下旋至叶子节点后删除
  97. //减少维护节点信息等复杂问题
  98. void del(int &p, int val)
  99. {
  100. if(p == 0) return;
  101. if(val == a[p].val)//检测到了val
  102. {
  103. if(a[p].cnt > 1)
  104. {
  105. a[p].cnt--, update(p); //直接减掉一个副本即可
  106. return; //退出
  107. }
  108. if(a[p].l || a[p].r)//不是叶子节点 向下旋转
  109. {
  110. if(a[p].r == 0 || a[a[p].l].dat > a[a[p].r].dat)
  111. {zig(p); del(a[p].r, val);} //右旋 后进入右子树
  112. else {zag(p); del(a[p].l, val);}
  113. update(p);//例行更新父节点信息
  114. }
  115. else p = 0; //是叶子节点 直接删除
  116. return;
  117. }
  118. val < a[p].val ? del(a[p].l, val) : del(a[p].r, val);
  119. update(p);
  120. }
  121. int GetPre(int val)
  122. { //前驱:小于x且最大的数
  123. int ans = 1; //a[1].val = -INF
  124. int p = root;
  125. while(p)
  126. {
  127. if(val == a[p].val) //找到了节点值相同的
  128. {
  129. if(a[p].l > 0) //如果有左子树
  130. {
  131. p = a[p].l; //不停向右走
  132. while(a[p].r > 0) p = a[p].r;
  133. ans = p;
  134. }
  135. break; //此时的ans为答案
  136. }
  137. //就算找不到val 前驱也被ans经过了
  138. if(a[p].val < val && a[p].val > a[ans].val) ans = p;
  139. p = val < a[p].val ? a[p].l : a[p].r;
  140. }
  141. return a[ans].val;
  142. }
  143. int GetNext(int val)
  144. { //后继:大于x且最小的数
  145. int ans = 2; // a[2].val = INF
  146. int p = root;
  147. while(p)
  148. {
  149. if(val == a[p].val)
  150. {
  151. if(a[p].r > 0)
  152. {
  153. p = a[p].r;
  154. while(a[p].l > 0) p = a[p].l;
  155. ans = p;
  156. }
  157. break;
  158. }
  159. if(a[p].val >val && a[p].val < a[ans].val) ans = p;
  160. p = val < a[p].val ? a[p].l : a[p].r;
  161. }
  162. return a[ans].val;
  163. }
  164. int main()
  165. {
  166. Build();
  167. scanf("%d", &n);
  168. int op, x;
  169. while(n--)
  170. {
  171. scanf("%d%d", &op, &x);
  172. if(op == 1)
  173. {//插入x数
  174. ins(root, x);
  175. }
  176. if(op == 2)
  177. {//删除x(若有多个x,只删除一个)
  178. del(root, x);
  179. }
  180. if(op == 3)
  181. {//查询x的排名(排名定义为比当前数小的个数+1)
  182. //若有多个相同的数 输出最小的排名
  183. printf("%d\n", GetRankByVal(root, x) - 1);
  184. }
  185. if(op == 4)
  186. {//查询排名为x的数
  187. printf("%d\n", GetValByRank(root, x + 1));
  188. }
  189. if(op == 5)
  190. {//求x的前驱(小于x且最大的数)
  191. printf("%d\n", GetPre(x));
  192. }
  193. if(op == 6)
  194. {//求x的后继(大于x且最小的数)
  195. printf("%d\n", GetNext(x));
  196. }
  197. }
  198. return 0;
  199. }

入门平衡树: Treap的更多相关文章

  1. hiho #1325 : 平衡树·Treap

    #1325 : 平衡树·Treap 时间限制:10000ms 单点时限:1000ms 内存限制:256MB 描述 小Ho:小Hi,我发现我们以前讲过的两个数据结构特别相似. 小Hi:你说的是哪两个啊? ...

  2. hiho一下103周 平衡树·Treap

    平衡树·Treap 时间限制:10000ms 单点时限:1000ms 内存限制:256MB 描述 小Ho:小Hi,我发现我们以前讲过的两个数据结构特别相似. 小Hi:你说的是哪两个啊? 小Ho:就是二 ...

  3. 算法模板——平衡树Treap 2

    实现功能:同平衡树Treap 1(BZOJ3224 / tyvj1728) 这次的模板有了不少的改进,显然更加美观了,几乎每个部分都有了不少简化,尤其是删除部分,这个参照了hzwer神犇的写法,在此鸣 ...

  4. 【山东省选2008】郁闷的小J 平衡树Treap

    小J是国家图书馆的一位图书管理员,他的工作是管理一个巨大的书架.虽然他很能吃苦耐劳,但是由于这个书架十分巨大,所以他的工作效率总是很低,以致他面临着被解雇的危险,这也正是他所郁闷的.具体说来,书架由N ...

  5. Hihocoder 1325 平衡树·Treap(平衡树,Treap)

    Hihocoder 1325 平衡树·Treap(平衡树,Treap) Description 小Ho:小Hi,我发现我们以前讲过的两个数据结构特别相似. 小Hi:你说的是哪两个啊? 小Ho:就是二叉 ...

  6. HihoCoder 1325 平衡树·Treap

    HihoCoder 1325 平衡树·Treap 时间限制:10000ms 单点时限:1000ms 内存限制:256MB 描述 小Ho:小Hi,我发现我们以前讲过的两个数据结构特别相似. 小Hi:你说 ...

  7. 普通平衡树Treap(含旋转)学习笔记

    浅谈普通平衡树Treap 平衡树,Treap=Tree+heap这是一个很形象的东西 我们要维护一棵树,它满足堆的性质和二叉查找树的性质(BST),这样的二叉树我们叫做平衡树 并且平衡树它的结构是接近 ...

  8. HihoCoder1325 : 平衡树·Treap(附STL版本)

    平衡树·Treap 时间限制:10000ms 单点时限:1000ms 内存限制:256MB 描述 小Ho:小Hi,我发现我们以前讲过的两个数据结构特别相似. 小Hi:你说的是哪两个啊? 小Ho:就是二 ...

  9. luoguP3369[模板]普通平衡树(Treap/SBT) 题解

    链接一下题目:luoguP3369[模板]普通平衡树(Treap/SBT) 平衡树解析 #include<iostream> #include<cstdlib> #includ ...

随机推荐

  1. Excel批量添加不同的批注

    Sub 批量添加不同批注() Dim rng As Range Dim i As String Range("A1:D1").ClearComments For Each rng ...

  2. mysql—增删改查

    MySQL数据库,每条命令后要加:号.不然会认为命令语句未输入完, 若在语句结尾不添加分号时, 命令提示符会以 -> 提示你继续输入(有个别特例, 但加分号是一定不会错的); show data ...

  3. 『炸弹 线段树优化建图 Tarjan』

    炸弹(SNOI2017) Description 在一条直线上有 N 个炸弹,每个炸弹的坐标是 Xi,爆炸半径是 Ri,当一个炸弹爆炸 时,如果另一个炸弹所在位置 Xj 满足: Xi−Ri≤Xj≤Xi ...

  4. Java的内存需要划分成为5个部分:

    Java的内存需要划分成为5个部分: 1.栈(Stack):存放的都是方法中的局部变量.方法的运行一定要在栈当中运行. 局部变量:方法的参数,或者是方法{}内部的变量 作用域:一旦超出作用域,立从栈内 ...

  5. k8s-Label(标签)

    k8s-Label(标签) 一.Label是什么? Label是Kubernetes系统中的一个核心概念.Label以key/value键值对的形式附加到各种对象上,如Pod.Service.RC.N ...

  6. Elastic Stack 7.5.0白金版永不过期

    适用版本:7.4.0~7.5.0 警告:本文章仅限于学习,非商业用途. 目录结构 # 先创建相关目录,具体结构如下: /opt |-- bulid # 编译目录 | |- src |-- instal ...

  7. python turtle画花

    turtle是一个功能强调大的绘图的库,可以用来绘制各种所需要的图案,但是在使用时需要计算好角度等一系列的问题. 代码(源自<Python语言程序设计>)如下: 运行结果:

  8. MethodInvoker委托,跨线程访问

    Invoke(new MethodInvoker(delegate { textBox1.Enabled = true; })); 上面是简单缩写,也可以写成 private void btnOK_C ...

  9. .NET / C# EF中的基础操作(CRUD)

    查 public List<users> Querys() { datatestEntities db = new datatestEntities(); var a = db.users ...

  10. 类嵌套_list泛型_餐馆点菜例

    form1内容: private void button1_Click(object sender, EventArgs e) { //声明并初始化一张点菜清单 yiduicai danzi = ne ...