最近闲来无事,就研究了下平衡树,写了个treap,最后搞出了数组模拟的版本,(宝宝真的不喜欢指针),大概就是这样子,写篇博客记录一下。
我们考虑一颗搜索二叉树,如果这颗二叉树长得很想一棵树,那么我们显然可以在Ologn的时间内查询元素,然而我们很多时候搜索二叉树可能会被卡成一条链,这样子复杂度就变成了On的了,显然是不科学的,所以我们要让这颗搜索二叉树尽可能的平衡。平衡有很多方式,这里我们讲一下treap。
treap顾名思义,就是树堆,我们想,如果随机出来很多数字,把他们组成一个堆,那么在理想情况下,这个堆应该是一个非常平衡的树的。所以我们在这里对于每一个点,我们不光维护他的data,还维护他随机出来的一个优先级。之后我们在保证搜索二叉树的情况下,保证堆的性质,就可以了。关键是如何保证堆的性质,显然我们需要对一些元素进行位置移动,所以我们要在这讲一下左旋和右旋。
图示是将pivot点左旋产生的效果,我们可以发现,在进行旋转后,并没有改变搜索二叉树的性质,并且还做到了元素的移动。
而右旋则是从右边变成左面。可以认为,左旋是让一个元素下降,右旋是让一个元素上升,我们可以借助他们做到维护堆的性质的同时,不改变二叉树的性质。
实现则十分简单,交换几个指针即可。(下文使用数组模拟指针)
然后我们来考虑一下,如何查询,这非常简单,我们只要按照搜索二叉树的性质即可,如果要搜索的值小于当前节点的值,就到左子树去找,否则到右侧。
我们来考虑如何删除一个元素,我们想一个节点无非有3种情况,要么没有子节点,直接删除指向他的指针即可。要么有一个子节点,这时候你直接把他删掉,上下接上即可。我们来考虑最复杂的第三种情况,我们考虑要删除的节点的左子节点的右子节点的……的右子节点,一定是,比要删除的点小的点中最大的。所以用这个点来替换要删除的点是毫无问题的。这里注意只替换值,不替换用于维护堆的性质的优先级。
大概就讲到这里,代码里有许多简化代码长度的小技巧,大家可以学习一下。
int s[siz][2], v[siz], pri[siz], tot, root;
int cmp(int a, int b)
{
if(v[a] == b) return -1;
return v[a] < b;
}
void rotate(int &x, int k)
{
int o = s[x][k];
s[x][k] = s[o][k^1];
s[o][k^1] = x;
x = o;
}
void insert(int &x, int k)
{
if(!x)
{
v[x = ++tot] = k;
pri[x] = rand();
return;
}
int t = cmp(x, k);
if(t != -1)
{
insert(s[x][t], k);
if(pri[s[x][t]] > pri[x]) rotate(x, t);
}
}
bool ask(int x, int k)
{
if(!x) return false;
int t = cmp(x, k);
if(t == -1) return true;
return ask(s[x][t], k);
}
void rem(int &);
void frem(int &x, int &k)
{
int cur = s[x][0];
if(!s[cur][1])
{
swap(v[cur], k);
rem(s[x][0]);
return;
}
while(s[s[cur][1]][1]) cur = s[cur][1];
swap(v[x],v[s[cur][1]]);
rem(s[cur][1]);
}
void rem(int &x)
{
if(!s[x][0] && !s[x][1])
{
x = 0;
return;
}
if(!s[x][0] ^ !s[x][1])
{
int t = s[x][1] > 0;
x = s[x][t];
return;
}
frem(x, v[x]);
}
void remove(int &x, int k)
{
if(!x) return;
int t = cmp(x, k);
if(t == -1)
{
rem(x);
return;
}
remove(s[x][t], k);
}