Treap 详解1
今天我刚刚学习了一个新的数据结构 - Treap!
Part 1.1 \text{Part 1.1} Part 1.1 Treap的作用
Treap 分为旋转 Treap 和无旋 Treap, 本文主要讲解旋转 Treap.
旋转 Treap 支持以下操作:
- 插入一个数 , 时间复杂度为 O ( h ) O(h) O(h).
- 删除一个数 , 时间复杂度为 O ( h ) O(h) O(h).
- 查找一个数 , 时间复杂度为 O ( h ) O(h) O(h).
- 查询某个数字的排名 , 时间复杂度为 O ( h ) O(h) O(h).
- 根据排名查询数字 , 时间复杂度为 O ( h ) O(h) O(h). (本文不讨论这个, 因为作者还不会)
其中 h h h 指的是树高, 在期望情况下, h = log N h = \log N h=logN, N N N 是节点的数量
但是实际上不是, 作者实测后, 在 N = 10 6 N=10^6 N=106 时, 树高平均是 51 51 51. 而 log 2 ( 10 6 ) = 19.1316 \log_2(10^6)=19.1316 log2(106)=19.1316
不过还是不错的, 因为普通的二叉查找树的树高是 N \sqrt{N} N 级别的, 而 N = 10 6 N=10^6 N=106 时平均树高是 1223 1223 1223(实测取平均值).
但是 set \text{set} set 的效率是我的 1.3 1.3 1.3 倍.(哭死)
Part 1.2 \text{Part 1.2} Part 1.2 Treap的节点定义
Treap 叫做树堆.
将 Tree 和 Heap 合成就是 Treap.
所以 Treap 的每一个节点都包含两个数字:
- 元素的值 ( value \text{value} value)
- 元素作为堆的时候的优先级 ( priority \text{priority} priority)
所以 Treap 是包含二叉搜索树和堆两种性质的树.
下面给出 Treap 的节点定义程序.
cpp
struct Node_t {
Node_t* rtSon[2]; // 两个儿子节点
int size, same_count, val, rank; // 子树大小, 相同个数, 值, 优先级
};
与堆不同, 二叉搜索树的堆的优先级是随机给出的.
因为 C 库的 rand 实在太慢, 下面给出一个 rand.
cpp
int _rand() {
static unsigned long long seed = 114514; // 表示静态变量, 函数运行后值会保留下来, 不是原来的值
seed ^= (seed << 13);
seed ^= (seed >> 7);
seed ^= ~(seed << 31); // 一通乱搞
int ret = seed;
return ret < 0 ? -ret : ret; // 如果是负数就变成正数
}
下面给出完整结构体, 请读者自己推敲.
cpp
int _rand() {
static unsigned long long seed = 114514;
seed ^= (seed << 13);
seed ^= (seed >> 7);
seed ^= ~(seed << 31);
int ret = seed;
return ret < 0 ? -ret : ret;
}
struct Node_t {
Node_t* rtSon[2]; // 两个儿子节点0=左1=右
int size, same_count, val, rank; // 子树大小, 这个值的相同个数, 值, 优先级
Node_t(int __v) : size(1), same_count(1), val(__v), rank(_rand()) {rtSon[0] = rtSon[1] = NULL;}
void maintain() {
// 这是一个维护函数, 维护这个子树的大小
size = same_count;
if (rtSon[0] != NULL) size += rtSon[0]->size; // 判断有没有左节点
if (rtSon[1] != NULL) size += rtSon[1]->size; // 判断有没有右节点
}
};
Part 1.3 \text{Part 1.3} Part 1.3 Treap的旋转
这个旋转是一切操作的基础, 非常之重要.
本质上就是让这个节点为根, 然后继续满足二叉搜索树的性质.
下面给出代码.
cpp
// 旋转节点 dir=0左旋 dir=1右旋
void rotate(Node_t* &root, int dir) {
Node_t* NewRoot = root->rtSon[dir ^ 1]; // 新的根节点
root->rtSon[dir ^ 1] = NewRoot->rtSon[dir];
NewRoot->rtSon[dir] = root; // 覆盖根节点
root->maintain(), NewRoot->maintain(); // 维护节点
root = NewRoot;
}
下面将进行旋转的模拟
A
/ \
B C
/ \
D E
首先考虑左旋(dir=0), 其实就是将节点 C 作为新的子树的根 (当前的根为 A).
- 节点
C就是节点A的右节点(rtSon[1]),所以dir ^ 1 = 1 - 然后将
C这个位置改为节点D. (代码中的root->rtSon[dir ^ 1] = NewRoot->rtSon[dir];) - 再将节点
C的左节点D改为节点A - 再覆盖掉根节点
现在,这个树变成了这个样子
C
/ \
A E
/ \
B D
形式化的讲:
- 左旋这个节点其实是将这个节点的右节点和根节点交换, 并且交换之后满足二叉搜索树的性质.
- 右旋同理, 就是将这个节点的左节点和根节点交换.
Part 1.4 \text{Part 1.4} Part 1.4 Treap的插入
这里我们设 r o o t root root 表示当前的节点, v a l val val 表示插入的数字.
- 情况1: 当前节点为空节点: 那么我们考虑新建一个节点, 这个节点的值就是 v a l val val. 然后插入成功
- 情况2: 当前 r o o t . v a l = = v a l root.val==val root.val==val, 那么我们考虑增加
root.same_count. 插入成功 - 情况3: 当前节点小于 v a l val val. 因为左子树 > 右子树, 我们就递归去左子树里面插入.
- 情况4: 当前节点大于 v a l val val. 同理, 我们递归到右子树里面
情况3和情况4可以使用一个通用的方法, 以减少代码量和出错的可能. 详见代码.
但是还有一种情况, 插入的节点违反了堆性质.
然后我们考虑堆的向上调整, 其实就是旋转上调这个节点, 可以自行的模拟.(详解上面旋转-形式化的说)
cpp
// 插入节点
void insert(Node_t* &root, int val) {
if (root == NULL) root = new Node_t(val);
else if (root->val == val) root->same_count++;
else {
int dir = val < root->val; // 位置
insert(root->rtSon[dir], val); // 插入节点
if (root->rank > root->rtSon[dir]->rank) rotate(root, dir ^ 1); // 这里可以参加 "形式化的说"
}
root->maintain(); // 维护节点
}
Part 1.5 \text{Part 1.5} Part 1.5 Treap的删除
删除应该也不难.
发现也可以像删除分成各种情况, 然后一一进行.
代码其实和插入很相似, 这里给出代码, 思考的时间留给读者.
cpp
// 删除节点
void erase(Node_t* &root, int val) {
// 值相等
if (root->val == val) {
if (root->same_count > 1) {root->same_count--; return ;} // 相同的节点直接减去 1 即可
if (root->rtSon[0] == NULL && root->rtSon[1] == NULL) {delete root; return ;} // 是叶子节点, 直接删去
if (root->rtSon[0] != NULL && root->rtSon[1] != NULL) {
// 均不为空的情况, 查看优先级
int dir = (root->rtSon[0]->rank < root->rtSon[1]->rank) ? 0 : 1;
rotate(root, dir); // 将这个节点向上移动
erase(root->rtSon[dir], val); // 继续删除根节点
return ;
}
Node_t* tmp = root;
if (root->rtSon[0] != NULL) root = root->rtSon[0]; // 只有一个节点为空, 可以直接交换
else root = root->rtSon[1];
delete tmp; // 删除节点
}
else erase(root->rtSon[root->val < val ? 0 : 1], val);
if (root != NULL) root->maintain(); // 维护节点大小
}
总结
这个数据结构并没有想象中的那么难, 希望读者可以学会这个东西.
然后这里就讲这么多, 代码有错误或说明有问题可以在评论区反馈给作者.
查询名次下次再写.
这里给出一个学习链接 https://oi-wiki.org/ds/treap/.