• Treap 原理详解和实战


    一 点睛

    Treap 指 Tree + heap,又叫作树堆,同时满足二叉搜索树和堆两种性质。二叉搜索树满足中序有序性,输入序列不同,创建的二叉搜索树也不同,在最坏的情况下(只有左子树或只有右子树)会退化为线性。例如输入1 2 3 4 5,创建的二叉搜索树如下图所示。

    二叉搜索树的插入、查找、删除等效率与树高成正比,因此在创建二叉搜索树时要尽可能通过调平衡压缩树高。平衡树有很多种,例如 AVL 树、伸展树、SBT、红黑树等,这些调平衡的方法相对复杂。

    若一个二叉搜索树插入节点的顺序是随机的,则得到的二叉搜索树在大多数情况下是平衡的,即使存在一些极端情况,这种情况发生的概率也很小,因此以随机顺序创建的二叉搜索树,其期望高度为O(logn )。可以将输入数据随机打乱,再创建二叉搜索树,但我们有时

    并不能事前得知所有待插入的节点,而 Treap 可以有效解决该问题。

    Treap 是一种平衡二叉搜索树,它给每个节点都附加了一个随机数,使其满足堆的性质,而节点的值又满足二叉搜索树的有序性,其基本操作的期望时间复杂度为 O(logn)。相对于其他平衡二叉搜索树,Treap 的特点是实现简单,而且可以基本实现随机平衡。

    在 Treap 的构建过程中,插入节点时会给每个节点都附加一个随机数作为优先级,该优先级满足堆的性质(最大堆或最小堆均可,这里以最大堆为例,根的优先级大于左右子节点),数值满足二叉搜索树性质(中序有序性,左子树大于根,右子树小于根)。

    输入 6 4 9 7 2,构建 Treap。首先给每个节点都附加一个随机数作为优先级,根据输入数据和附加随机数,构建的 Treap 如下图所示。

    二 右旋和左旋

    Treap 需要两种旋转操作:右旋和左旋。

    1 右旋(zig)

    节点 p 右旋时,会携带自己的右子树,向右旋转到 q 的右子树位置,q 的右子树被抛弃,此时 p 右旋后左子树正好空闲,将 q 的右子树放在 p 的左子树位置,旋转后的树根为 q 。

    2 左旋(zag)

    节点 p 左旋时,携带自己的左子树,向左旋转到 q 的左子树位置,q 的左子树被抛弃,此时 p 左旋后右子树正好空闲,将 q 的左子树放在 p 的右子树位置,旋转后的树根为 q 。

    总结:无论是右旋还是左旋,旋转后总有一棵子树被抛弃,一个指针空闲,正好配对。

    三 插入

    Treap 的插入操作和二叉搜索树一样,首先根据有序性找到插入的位置,然后创建新节点插入该位置。创建新节点时,会给该节点附加一个随机数作为优先级,自底向上检查该优先级是否满足堆性质,若不满足,则需要右旋或左旋,使其满足堆性质。

    算法步骤如下。

    (1)从根节点 p 开始,若 p 为空,则创建新节点,将待插入元素 val 存入新节点,并给新节点附加一个随机数作为优先级。

    (2)若 val 等于 p 的值,则什么都不做,返回。

    (3)若 val 小于 p 的值,则在 p 的左子树中递归插入。回溯时做旋转调整,若 p 的优先级小于其左子节点的优先级,则 p 右旋。

    (4)若 val 大于 p 的值,则在 p 的右子树中递归插入。回溯时做旋转调整,若 p 的优先级小于其右子节点的优先级,则 p 左旋。

    一个树堆如下图所示,在该树堆中插入元素 8,插入过程如下。

    (1)根据二叉搜索树的插入操作,将 8 插入 9 的左子节点位置,假设 8 的随机数优先级为 25016。

    (2)回溯时,判断是否需要旋转,9 的优先级比其左子节点小,因此 9 节点右旋。

    (3)继续向上判断,7 的优先级比 7 的右子节点小,因此7节点左旋。

    (4)继续向上判断,6 的优先级不比 6 的左右子节点小,满足最大堆性质,无须调整,已向上判断到树根,算法停止。

    四 删除

    Treap 的删除操作非常简单:找到待删除的节点,将该节点向优先级大的子节点旋转,一直旋转到叶子,直接删除叶子即可。

    算法步骤如下。

    (1)从根节点 p 开始,若待删除元素 val 等于p 的值,则:若 p 只有左子树或只有右子树,则令其子树子承父业代替 p ,返回;若 p 的左子节点优先级大于右子节点的优先级,则 p 右旋,继续在 p 的右子树中递归删除;若 p 的左子节点的优先级小于右子节点的优先级,则 p 左旋,继续在 p 的左子树中递归删除。

    (2)若 val 小于 p 的值,则在 p 的左子树中递归删除。

    (3)若 val 大于 p 的值,则在 p 的右子树中递归删除。

    在上面的树堆中删除元素 8,删除过程如下。

    (1)根据二叉搜索树的删除操作,首先找到 8 的位置,8 的右子节点优先级大,8 左旋。

    (2)接着判断,8 的左子节点优先级大,8 右旋。

    (3)此时 8 只有一个左子树,左子树子承父业代替它。

    五 前驱

    在 Treap 中求一个节点 val 的前驱时,首先从树根开始,若当前节点的值小于 val,则用 res 暂存该节点的值,在当前节点的右子树中寻找,否则在当前节点的左子树中寻找,直到当前节点为空,返回 res,即为 val 的前驱。

    六 后继

    在 Treap 中求一个节点 val 的后继时,首先从树根开始,若当前节点的值大于 val,则用 res 暂存该节点的值,在当前节点的左子树中寻找,否则在当前节点的右子树中寻找,直到当前节点为空,返回 res,即为 val 的后继。

    七 代码

    1. package com.platform.modules.alg.alglib.p366;
    2. import java.util.Random;
    3. public class P366 {
    4. public String output = "";
    5. private int maxn = 100005;
    6. int n; // 结点数
    7. int cnt; // 结点存储下标累计
    8. int root; // 树根
    9. private node tr[] = new node[maxn];
    10. // 生成新结点
    11. int New(int val) {
    12. tr[++cnt].val = val;
    13. tr[cnt].pri = Math.abs(new Random().nextInt()) % 100;
    14. tr[cnt].num = tr[cnt].size = 1;
    15. tr[cnt].rc = tr[cnt].lc = 0;
    16. return cnt;
    17. }
    18. // 更新子树大小
    19. void Update(int p) {
    20. tr[p].size = tr[tr[p].lc].size + tr[tr[p].rc].size + tr[p].num;
    21. }
    22. // 右旋
    23. int zig(int p) {
    24. int q = tr[p].lc;
    25. tr[p].lc = tr[q].rc;
    26. tr[q].rc = p;
    27. tr[q].size = tr[p].size;
    28. Update(p);
    29. // 现在 q 为根
    30. p = q;
    31. return p;
    32. }
    33. // 左旋
    34. int zag(int p) {
    35. int q = tr[p].rc;
    36. tr[p].rc = tr[q].lc;
    37. tr[q].lc = p;
    38. tr[q].size = tr[p].size;
    39. Update(p);
    40. // 现在 q 为根
    41. p = q;
    42. return p;
    43. }
    44. int Insert(int p, int val) // 在 p 的子树插入值val
    45. {
    46. if (p == 0) {
    47. p = New(val);
    48. return p;
    49. }
    50. tr[p].size++;
    51. if (val == tr[p].val) {
    52. tr[p].num++;
    53. return p;
    54. }
    55. if (val < tr[p].val) {
    56. tr[p].lc = Insert(tr[p].lc, val);
    57. if (tr[p].pri < tr[tr[p].lc].pri)
    58. p = zig(p);
    59. } else {
    60. tr[p].rc = Insert(tr[p].rc, val);
    61. if (tr[p].pri < tr[tr[p].rc].pri)
    62. p = zag(p);
    63. }
    64. return p;
    65. }
    66. int Delete(int p, int val) // 在p的子树删除值val
    67. {
    68. if (p == 0)
    69. return p;
    70. tr[p].size--;
    71. if (val == tr[p].val) {
    72. if (tr[p].num > 1) {
    73. tr[p].num--;
    74. return p;
    75. }
    76. if (tr[p].lc == 0 || tr[p].rc == 0)
    77. p = tr[p].lc + tr[p].rc; // 有一个儿子为空,直接用儿子代替
    78. else if (tr[tr[p].lc].pri > tr[tr[p].rc].pri) {
    79. p = zig(p);
    80. tr[p].rc = Delete(tr[p].rc, val);
    81. } else {
    82. p = zag(p);
    83. tr[p].lc = Delete(tr[p].lc, val);
    84. }
    85. return p;
    86. }
    87. if (val < tr[p].val)
    88. tr[p].lc = Delete(tr[p].lc, val);
    89. else
    90. tr[p].rc = Delete(tr[p].rc, val);
    91. return p;
    92. }
    93. // 找前驱
    94. int GetPre(int val) {
    95. int p = root;
    96. int res = -1;
    97. while (p > 0) {
    98. if (tr[p].val < val) {
    99. res = tr[p].val;
    100. p = tr[p].rc;
    101. } else
    102. p = tr[p].lc;
    103. }
    104. return res;
    105. }
    106. // 找后继
    107. int GetNext(int val) {
    108. int p = root;
    109. int res = -1;
    110. while (p > 0) {
    111. if (tr[p].val > val) {
    112. res = tr[p].val;
    113. p = tr[p].lc;
    114. } else
    115. p = tr[p].rc;
    116. }
    117. return res;
    118. }
    119. public P366() {
    120. for (int i = 0; i < tr.length; i++) {
    121. tr[i] = new node();
    122. }
    123. }
    124. void print(int p) {
    125. if (p > 0) {
    126. print(tr[p].lc);
    127. output += tr[p].val + " " + tr[p].pri + " " + tr[p].num + " " + tr[p].size + " " + tr[tr[p].lc].val + " " + tr[tr[p].rc].val + " " + "\n";
    128. print(tr[p].rc);
    129. }
    130. }
    131. public String cal(String input) {
    132. String[] line = input.split("\n");
    133. // 初始化节点
    134. n = Integer.parseInt(line[0]);
    135. String[] nums = line[1].split(" ");
    136. for (int i = 1; i <= n; i++) {
    137. root = Insert(root, Integer.parseInt(nums[i - 1]));
    138. }
    139. print(root);
    140. int opt, x;
    141. int count = 2;
    142. while (true) {
    143. String[] command = line[count++].split(" ");
    144. opt = Integer.parseInt(command[0]);
    145. switch (opt) {
    146. case 0:
    147. return output;
    148. case 1:
    149. x = Integer.parseInt(command[1]);
    150. root = Insert(root, x);
    151. print(root);
    152. break;
    153. case 2:
    154. x = Integer.parseInt(command[1]);
    155. root = Delete(root, x);
    156. print(root);
    157. break;
    158. case 3:
    159. x = Integer.parseInt(command[1]);
    160. output += GetPre(x) + "\n";
    161. break;
    162. case 4:
    163. x = Integer.parseInt(command[1]);
    164. output += GetNext(x) + "\n";
    165. break;
    166. }
    167. }
    168. }
    169. }
    170. class node {
    171. int lc, rc; // 左右孩子
    172. int val, pri; // 值,优先级
    173. int num, size; // 重复个数,根的子树的大小
    174. }

    八 测试

    1 输入

    5

    6 2 7 4 9

    1 8

    2 8

    3 7

    4 2

    0

    2 输出

    2 77 1 3 0 4

    4 18 1 2 0 6

    6 1 1 1 0 0

    7 88 1 5 2 9

    9 61 1 1 0 0

    2 77 1 3 0 4

    4 18 1 2 0 6

    6 1 1 1 0 0

    7 88 1 6 2 8

    8 77 1 2 0 9

    9 61 1 1 0 0

    2 77 1 3 0 4

    4 18 1 2 0 6

    6 1 1 1 0 0

    7 88 1 5 2 9

    9 61 1 1 0 0

    6

    4

  • 相关阅读:
    七夕当然要学会SQL优化好早点下班去找对象
    7-2 图着色问题
    MySQL内连接和外连接及七种SQL JOINS的实现
    大厂超全安全测试--关于安全测试的分类及如何测试
    非常实用的Visual Studio Code快捷键(2) 欢迎各位大侠补充
    三天学会阿里分布式事务框架Seata-SpringCloud Alibaba分布式基础案例搭建
    前端:用Sass简化媒体查询
    基于 Kafka 的实时数仓在搜索的实践应用
    【LeetCode热题100】--347.前K个高频元素
    3D设计软件Rhinoceros 6 mac 犀牛6中文版功能特征
  • 原文地址:https://blog.csdn.net/chengqiuming/article/details/127965507