一 点睛
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 的后继。
七 代码
package com.platform.modules.alg.alglib.p366;
import java.util.Random;
public class P366 {
public String output = "";
private int maxn = 100005;
int n; // 结点数
int cnt; // 结点存储下标累计
int root; // 树根
private node tr[] = new node[maxn];
// 生成新结点
int New(int val) {
tr[++cnt].val = val;
tr[cnt].pri = Math.abs(new Random().nextInt()) % 100;
tr[cnt].num = tr[cnt].size = 1;
tr[cnt].rc = tr[cnt].lc = 0;
return cnt;
}
// 更新子树大小
void Update(int p) {
tr[p].size = tr[tr[p].lc].size + tr[tr[p].rc].size + tr[p].num;
}
// 右旋
int zig(int p) {
int q = tr[p].lc;
tr[p].lc = tr[q].rc;
tr[q].rc = p;
tr[q].size = tr[p].size;
Update(p);
// 现在 q 为根
p = q;
return p;
}
// 左旋
int zag(int p) {
int q = tr[p].rc;
tr[p].rc = tr[q].lc;
tr[q].lc = p;
tr[q].size = tr[p].size;
Update(p);
// 现在 q 为根
p = q;
return p;
}
int Insert(int p, int val) // 在 p 的子树插入值val
{
if (p == 0) {
p = New(val);
return p;
}
tr[p].size++;
if (val == tr[p].val) {
tr[p].num++;
return p;
}
if (val < tr[p].val) {
tr[p].lc = Insert(tr[p].lc, val);
if (tr[p].pri < tr[tr[p].lc].pri)
p = zig(p);
} else {
tr[p].rc = Insert(tr[p].rc, val);
if (tr[p].pri < tr[tr[p].rc].pri)
p = zag(p);
}
return p;
}
int Delete(int p, int val) // 在p的子树删除值val
{
if (p == 0)
return p;
tr[p].size--;
if (val == tr[p].val) {
if (tr[p].num > 1) {
tr[p].num--;
return p;
}
if (tr[p].lc == 0 || tr[p].rc == 0)
p = tr[p].lc + tr[p].rc; // 有一个儿子为空,直接用儿子代替
else if (tr[tr[p].lc].pri > tr[tr[p].rc].pri) {
p = zig(p);
tr[p].rc = Delete(tr[p].rc, val);
} else {
p = zag(p);
tr[p].lc = Delete(tr[p].lc, val);
}
return p;
}
if (val < tr[p].val)
tr[p].lc = Delete(tr[p].lc, val);
else
tr[p].rc = Delete(tr[p].rc, val);
return p;
}
// 找前驱
int GetPre(int val) {
int p = root;
int res = -1;
while (p > 0) {
if (tr[p].val < val) {
res = tr[p].val;
p = tr[p].rc;
} else
p = tr[p].lc;
}
return res;
}
// 找后继
int GetNext(int val) {
int p = root;
int res = -1;
while (p > 0) {
if (tr[p].val > val) {
res = tr[p].val;
p = tr[p].lc;
} else
p = tr[p].rc;
}
return res;
}
public P366() {
for (int i = 0; i < tr.length; i++) {
tr[i] = new node();
}
}
void print(int p) {
if (p > 0) {
print(tr[p].lc);
output += tr[p].val + " " + tr[p].pri + " " + tr[p].num + " " + tr[p].size + " " + tr[tr[p].lc].val + " " + tr[tr[p].rc].val + " " + "\n";
print(tr[p].rc);
}
}
public String cal(String input) {
String[] line = input.split("\n");
// 初始化节点
n = Integer.parseInt(line[0]);
String[] nums = line[1].split(" ");
for (int i = 1; i <= n; i++) {
root = Insert(root, Integer.parseInt(nums[i - 1]));
}
print(root);
int opt, x;
int count = 2;
while (true) {
String[] command = line[count++].split(" ");
opt = Integer.parseInt(command[0]);
switch (opt) {
case 0:
return output;
case 1:
x = Integer.parseInt(command[1]);
root = Insert(root, x);
print(root);
break;
case 2:
x = Integer.parseInt(command[1]);
root = Delete(root, x);
print(root);
break;
case 3:
x = Integer.parseInt(command[1]);
output += GetPre(x) + "\n";
break;
case 4:
x = Integer.parseInt(command[1]);
output += GetNext(x) + "\n";
break;
}
}
}
}
class node {
int lc, rc; // 左右孩子
int val, pri; // 值,优先级
int num, size; // 重复个数,根的子树的大小
}
八 测试
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