1. 基本知识
1.1 Treap
Treap(树堆) 是一种弱平衡的二叉搜索树,每个节点的权值具有二叉搜索树的性质,优先级具有堆的性质
具体来说,树堆满足如下性质
- 左子结点的权值小于根节点的权值
- 右子节点的权值大于根节点的权值
- 子节点的优先级(注意,不是权值!)都大于/都小于根节点的优先级(取决于大根堆/小根堆)
如下图即为一个 Treap 的例子(采用小根堆)——
注:黑色数字为权值,棕色数字为优先级
为什么相较于 BST,需要对每个节点多维护一个优先级?
对每个节点,维护满足堆性质的优先级,是为了保证树尽量平衡:
对于一棵 每层节点较多 的二叉搜索树,查找时需要递归的次数便较小,复杂度为
反之,退化成一条链 是二叉搜索树的最坏情况,此时查找所需复杂度为线性,而优先级的设置便防止了这种情况的出现
1.2 FHQ-Treap
FHQ-Treap 又称分裂合并 Treap,无须旋转,只需支持分裂和合并两种基本操作
FHQ-Treap 支持可持久化和区间操作,但此处仅介绍基本操作
2. 初始化 FHQ-Treap
在 FHQ-Treap 的节点上维护如下属性与方法(采用指针式):
- 左右子节点的地址
- 当前节点的权值
- 当前节点的优先级
- 当前节点的出现次数
- 当前节点的子树大小
- 重新计算子树大小的方法
于是可以得到节点的代码:
struct Node {
Node *ch[2]; // 左右孩子的指针
int val; // 权值
int rank; // 优先级
int rep_cnt; // 出现次数
int siz; // 子树大小
Node (int _val) : val(_val), rep_cnt(1), siz(1) { // 构造函数
ch[0] = ch[1] = nullptr;
rank = rand(); // 随机取优先级
}
void upd_siz() { // 重新计算子树大小的方法
siz = rep_cnt;
if (ch[0] != nullptr) {
siz = siz + ch[0]->siz; // 加上左子树的大小
}
if (ch[1] != nullptr) {
siz = siz + ch[1]->siz; // 加上右子树的大小
}
}
};
3. 分裂
3.1 按值分裂
分裂过程接收 根节点的指针 及 关键值 两个参数,返回两个指针,分别指向分裂后的 两个 Treap,第一个 Treap 所有节点的值小于等于关键值,第二个 Treap 所有节点的值大于关键值
对于要分裂的关键值 (),大致分裂思路如下:
- 首先比较根节点的值与关键值
- 如果根节点的值小于等于关键值,说明根节点及其左子树上的所有节点均小于等于关键值,而此时根节点的右子树上仍可能有节点小于等于关键值,因此递归分裂右子树 (结果记为
),再将当前根节点的右孩子指向
,最终返回根节点的指针与
- 如果根节点的值大于关键值,说明根节点及其右子树上的所有节点均大于关键值,而此时根节点的左子树上仍可能有节点大于关键值,因此递归分裂左子树 (结果记为
),再将当前根节点的左孩子指向
,最终返回
与根节点的指针
于是可以得到按值分裂的代码:
pair<Node *, Node *> split(Node *root, int key) { // 根节点指针,关键值
if (root == nullptr) { // 当前节点不存在
return {nullptr, nullptr};
}
if (root->val <= key) { // 根节点的值小于等于关键值
auto temp = split(root->ch[1], key); // 递归分裂右子树
root->ch[1] = temp.first;
root->upd_siz(); // 别忘了更新子树大小
return {root, temp.second};
} else { // 根节点的值大于关键值
auto temp = split(root->ch[0], key); // 递归分裂左子树
root->ch[0] = temp.second;
root->upd_siz(); // 别忘了更新子树大小
return {temp.first, root};
}
}
3.2 按排名分裂
排名定义为,比当前权值小的权值的个数 +1(注意重复的也算哦)
分裂过程接收 根节点的指针 及 排名