描述
小Ho:小Hi,我发现我们以前讲过的两个数据结构特别相似。
小Hi:你说的是哪两个啊?
小Ho:就是二叉排序树和堆啊,你看这两种数据结构都是构造了一个二叉树,一个节点有一个父亲和两个儿子。 如果用1…n的数组来存储的话,对于二叉树上的一个编号为k的节点,其父亲节点刚好是k/2。并且它的两个儿子节点分别为k2和k2+1,计算起来非常方便呢。
小Hi:没错,但是小Hi你知道有一种办法可以把堆和二叉搜索树合并起来,成为一个新的数据结构么?
小Ho:这我倒没想过。不过二叉搜索树满足左子树<根节点<右子树,而堆是满足根节点小于等于(或大于等于)左右儿子。这两种性质是冲突的啊?
小Hi:恩,你说的没错,这两种性质的确是冲突的。
小Ho:那你说的合并是怎么做到的?
小Hi:当然有办法了,其实它是这样的…
提示:Tree+Heap?
输入
第1行:1个正整数n,表示操作数量,10≤n≤100,000
第2…n+1行:每行1个字母c和1个整数k:
若c为’I’,表示插入一个数字k到树中,-1,000,000,000≤k≤1,000,000,000
若c为’Q’,表示询问树中不超过k的最大数字
输出
若干行:每行1个整数,表示针对询问的回答,保证一定有合法的解
样例输入
5
I 3
I 2
Q 3
I 5
Q 4
样例输出
3
3
思路:
小Hi:由于是二叉搜索树和堆合并构成的新数据结构,所以它的名字取了Tree和Heap各一半,叫做Treap。在有的教材中,把Treap翻译成树堆。
正如你刚刚所说,堆和树的性质是冲突的。因此在Treap的数据结构中,并不是以单一的键值作为节点的数据域。
Treap每个节点的数据域包含2个值,key和weight。
key值,和原来的二叉搜索树一样,满足左子树<根节点<右子树。
weight值,随机产生。在Treap中weight值满足堆的性质,根节点的weight值小于等于(或大于等于)左右儿子节点。
比如下图就是一个示例的Treap:
小Ho:感觉上就是在原来二叉搜索树上增加了一个weight值。
小Hi:简单来理解的话,就是这样。
因此Treap的大部分操作都和二叉搜索树是一样的,唯一区别在于每次插入一个节点后,需要对树的结构进行调整。
因为每一个节点的weight值不一样,当我们按照key值插入一个节点后,这个节点有可能不满足weight值的要求。
对于如何调整,首先我们来看一个最简单的例子:
如图所示的一个Treap有三个节点,其中根的右儿子节点是新插入的。
假设我们一开始想要让Treap满足小根堆的性质,即weight值越小越在堆顶。
那么我们需要在不改变key值顺序的情况下,对节点进行变形,使得weight值满足性质。
这一步骤被称为旋转,对于例子,其旋转之后的形态为:
根据旋转的方向不同,旋转分为两种:左旋和右旋。
在例子中是将右儿子节点旋转至根,所以称为左旋。反之将左儿子节点旋转至根,称为右旋。
那么这个旋转具体的过程,我们可以对应旋转前后的图来分析。首先是左旋操作:
它的过程有如下几步:
- 获取根节点A的右儿子节点B
- 将节点B的父亲节点信息更新为f,并更新节点f的子节点信息为B
- 将节点A的右儿子信息更新为节点B的左儿子D,同时将节点D的父亲节点信息更新为A
- 将节点B的左儿子信息更改为节点A,同时将节点A的父亲节点信息更改为B
该过程的伪代码如下:
left-rotate(a):
b = a.right
b.father = a.father
If (a.father.left == a) Then
a.father.left = b
Else
a.father.right = b
End If
a.right = b.left
b.left.father = a
b.left = a
a.father = b
然后是右旋操作:
其过程是左旋操作的镜像:
- 获取根节点A的左儿子节点B
- 将节点B的父亲节点信息更新为f,并更新节点f的子节点信息为B
- 将节点A的左儿子信息更新为节点B的右儿子D,同时将节点D的父亲节点信息更新为A
- 将节点B的右儿子信息更改为节点A,同时将节点A的父亲节点信息更改为B
该过程的伪代码如下:
right-rotate(a):
b = a.left
b.father = a.father
If (a.father.left == a) Then
a.father.left = b
Else
a.father.right = b
End If
a.left = b.right
b.right.father = a
b.right = a
a.father = b
小Ho:旋转的操作我是理解了,但是要怎么运用呢?
小Hi:只要将节点插入Treap以后,再不断的旋转当前节点直到weight满足堆的性质。
首先我们从插入操作来看,这里我们让insert完成后返回新加入的节点:
insert(node, key):
If (key < node.key) Then
If (node.left is empty) Then
node.left = {key: key, weight: random()}
Return node.left
Else
Return insert(node.left)
End If
Else
If (node.right is empty) Then
node.right = {key: key, weight: random()}
Return node.right
Else
Return insert(node.right)
End If
End If
完成插入操作后,我们获得了新加入的节点,然后迭代的进行旋转(这里假设采用小根堆):
rotate(node):
While (node.father is not empty)
fa = node.father
If (node.weight < fa.weight) Then
If (node == fa.left) Then
right-rotate(fa)
// 这里的参数是父节点
// 在该处fa对应了旋转中的节点a, node对应了节点b
Else
left-rotate(fa)
End If
Else
Break
End If
End While
需要注意的是,对于我这种插入的写法,需要手动在Treap中插入第一个元素:
main():
root = NULL
For i = 1 .. n
input x
If (root is empty)
root = {key: x, weight: random()}
Else
rotate(insert(root, x))
End If
End For
另外还有一点,相比较于普通的二叉搜索树,Treap删除节点的操作也有一定的区别。
同样需要根据删除节点的孩子数量来进行处理:
-
没有孩子节点,则当前结点为叶子节点,直接删去即可。
-
有一个孩子节点,和普通二叉搜索树相同,让孩子节点代替当前节点。
-
有两个孩子节点,利用旋转,将weight值小(或大)的子节点旋转到根上,将待删除节点向下旋转。反复操作直到待删除节点只有0个或1个子节点。
其伪代码为:
delete(key):
node = find(key)
// 这里的find操作和普通的二叉搜索树相同
While (node's child is full)
// 从儿子节点中找到weight值小的一个旋转至根
child = node.left // 先初始为左节点较小
If (child.weight > node.right.weight)
// 比较左右节点
child = node.right
End If
// 进行旋转
If (child == node.left) Then
right-rotate(node)
Else
left-rotate(node)
End If
End While
// 此时当前结点只有一个或零个儿子
fa = node.fa
If (node.left is not empty) Then
// 当前结点只有一个左儿子
node.left.fa = fa
If (node == fa.left) Then
fa.left = node.left
Else
fa.right = node.left
End If
Elseif (node.right is not empty) Then
// 当前结点只有一个右儿子
node.right.fa = fa
If (node == fa.left) Then
fa.left = node.right
Else
fa.right = node.right
End If
Else
// 已经旋转到叶子节点,删去当前结点
If (node == fa.left) Then
fa.left = NULL
Else
fa.right = NULL
End If
End If
小Ho:我明白了,但是总的来说Treap和普通的二叉搜索树实现的功能是一样的,为什么要花费额外的时间去做旋转操作呢?
小Hi:对于一般的二叉搜索树,在某些特殊情况下根据输入数据来建树有可能退化为一条链,比如一个依次增大的数列。
而如果一棵二叉排序树的节点是按照随机顺序插入,得到的二叉排序树大多数情况下是平衡的,其期望高度是O(logn)。
因此Treap利用weight值作为随机因子来调整二叉树的形状,使得在大部分情况下比直接通过数据建立的二叉树要平衡。
每一次查找的期望复杂度也会降低,总体的速度也就得到了提高。
#include <bits/stdc++.h>
using namespace std;
const int mod = 1e9+7;
struct Treap {
int key;
int weight;
Treap* left;
Treap* right;
Treap* father;
Treap(int k, int w) {
key = k;
weight = w;
left = nullptr;
right = nullptr;
father = nullptr;
}
};
void leftRotate(Treap* a) {
Treap* b = a->right;
b->father = a->father;
if (a->father->left == a) {
a->father->left = b;
}
else {
a->father->right = b;
}
a->right = b->left;
b->left->father = a;
b->left = a;
a->father = b;
}
void rightRotate(Treap* a) {
Treap* b = a->left;
b->father = a->father;
if (a->father->left == a) {
a->father->left = b;
}
else {
a->father->right = b;
}
a->left = b->right;
b->right->father = a;
b->right = a;
a->father = b;
}
Treap* Insert(Treap* node, int key) {
if (key < node->key) {
if (node->left == nullptr) {
Treap* tmp = new Treap(key,rand()%mod);
node->left = tmp;
return node->left;
}
else {
return Insert(node->left, key);
}
}
else {
if (node->right == nullptr) {
Treap* tmp = new Treap(key,rand()%mod);
node->right = tmp;
return node->right;
}
else {
return Insert(node->right, key);
}
}
}
void rotate(Treap* node) {
while(node->father != nullptr) {
Treap* fa = node->father;
if (node->weight < fa->weight) {
if (node == fa->left) {
rightRotate(fa);
}
else
leftRotate(fa);
}
else
break;
}
}
void Search(Treap* cur, int x, int &res) {
if (cur == nullptr) return;
if (cur->key == x) {
res = x;
return;
}
if (cur->key > x) {
Search(cur->left, x, res);
}
else {
res = cur->key;
Search(cur->right, x, res);
}
}
int main() {
int n;
cin >> n;
char op;
int x;
int res = 0;
Treap* root = nullptr;
while(n--) {
cin >> op >> x;
if (op == 'I') {
if (root == nullptr) {
root = new Treap(x, rand()%mod);
}
else
rotate(Insert(root, x));
}
else {
Search(root, x, res);
cout << res << endl;
}
}
return 0;
}
简单解法
其实还有一种简单的解法,那就是直接利用stl封装好的容器和算法。利用set和upper_bound()
#include<bits/stdc++.h>
using namespace std;
int main()
{
int n, x;
char op;
set<int> s;
set<int>::iterator it;
cin >> n;
while(n--) {
cin >> op >> x;
if(op=='I') {
s.insert(x);
}
else {
it=s.upper_bound(x);
cout << *--it << endl;
}
}
}