平衡树Treap
概念
Treap=Tree+Heap,是二叉搜索树和堆的结合体。Treap本身是一棵二叉搜索树,它的左子树和右子树也分别是一个Treap,和一般的二叉搜索树不同的是, Treap会记录一个额外的信息,就是优先级。Treap在以关键码构成二叉搜索树的同时,还满足堆的性质,具体来说它只满足堆的一个性质:对于大根堆,根节点的优先级大于左右孩子的优先级。对于小根堆,根节点的优先级小于左右孩子的优先级。 这些优先级是是在插入节点时,随机赋予的,Treap根据这些优先级满足堆的性质。因此,Treap是有一个随机附加域满足堆的性质的二叉搜索树。Treap维护堆性质的方法只用到了旋转,只需要两种旋转,即左旋和右旋。
在随机数据下,普通的BST(即二叉搜索树)是趋向于平衡的。Treap的思想就是利用"随机"来创造平衡条件。因为在旋转过程中必须保持BST性质,所以Treap就把"随机"作用在堆性质上。
Treap在插入每一个新节点时,给该节点随机生成一个额外的权值,然后像二叉堆的插入一样,自底向上检查,当某个节点不满足大根堆的性质时,就执行单旋转,使该节点与其父节点的关系发生对换。对于删除操作,因为Treap支持旋转,我们可以直接找到需要删除的节点,并把它向下旋转成叶子节点,最后直接删除。
总之,Treap通过适当的单旋转,在维持节点关键码满足BST性质的同时,还使得每个节点上随机生成的额外权值满足大根堆的性质。Treap是一种平衡二叉查找树,检索、插入、求前驱后继、删除节点的时间复杂度都是 O ( l o g n ) O(logn) O(logn)。
结构体
const int N=100010;
struct Node{
int l,r; //左右孩子节点在数组中的下标
int key; //该节点的关键码
int val; //该节点的优先级
int cnt; //该节点中包含多少个相同的关键码 比如1号节点中有关键码99 99 99 那么cnt=3
int size; //记录以某个节点为根的子树中总共包含多少个节点
}tr[N]; //用数组模拟链表
插入
给节点随机分配一个优先级,先和二叉搜索树的插入一样,先把要插入的节点插入到一个叶子节点上,然后 跟维护堆一样,如果当前节点的优先级比根大就旋转,若当前节点是根的左儿子就右旋,如果当前节点是根的右儿子就左旋。左旋能使原来的根节点转移到右边,右旋能使原来的根节点转移到左边。
如下图所示:数字越小,表示优先级就越大。
插入代码
void insert(int &p,int key) //最终实现在叶子节点插入关键码为key
{
//如果p就是叶子节点,那正好,直接插入这个关键码为key的叶子节点就行了
if(p==0)
p=get_node(key);
//如果想要插入的这个key值和当前p节点的关键码相等,说明p节点中又多了一个相同的关键码,cnt+1
else if(tr[p].key==key)
tr[p].cnt++;
//如果想要插入的这个key值比当前p节点的关键码还小,由二叉搜索树可知,此时key值应该是在p节点的左子树
else if(tr[p].key>key)
{
insert(tr[p].l,key); //tr[p].l是p节点的左子节点的下标 递归去左子树插入这个key值
//如果key值来到左子树后,发现p节点的左子节点的优先级大于p节点的优先级,破坏了堆的性质,则需要右旋
if(tr[tr[p].l].val>tr[p].val)
zig(p);
}
//如果想要插入的这个key值比当前p节点的关键码还大,由二叉搜索树可知,此时key值应该是在p节点的右子树
else
{
insert(tr[p].r,key); //tr[p].r是p节点的右子节点的下标 递归去右子树插入这个key值
//如果key值来到右子树后,发现p节点的右子节点的优先级大于p节点的优先级,破坏了堆的性质,则需要左旋
if(tr[tr[p].r].val>tr[p].val)
zag(p);
}
pushup(p);//插入key值后,由下往上更新节点信息
}
左旋代码
//这里用引用是因为根节点的指针会改变,因此需要引用带回它后来的地址
void zag(int &p)
{
int q=a[p].r; //此时q是p的右孩子
a[p].r=a[q].l; //左旋后,原来根节点(x)它的右孩子(y)的左子树就成为原来根节点(x)的右子树。
a[q].l=p; //左旋后,原来的根节点(x)就成为新根节点(y)的左孩子
p=q; //让p重新指向这个子树的新根节点y
}
右旋代码
右旋代码其实就是将左旋代码中有L的地方都换成R,有R的地方都换成L,其他地方不用做改变即可
//这里用引用是因为根节点的指针会改变,因此需要引用带回它后来的地址
void zig(int &p)
{
int q=a[p].l; //此时q是p的左孩子
a[p].l=a[q].r; //右旋后,原来根节点(y)它的左孩子(x)的右子树就成为了原来根节点(y)的左子树
a[q].r=p; //右旋后,原来的根节点(y)就成为新根节点(x)的右孩子
p=q; //让p重新指向这个子树的新根节点x
}
BST的建立
为了避免边界,减少对边界情况的特殊判断,我们一般在BST中额外插入一个关键码为正无穷(一个很大的正整数)和一个操作码为负无穷(一个很小的负整数)的节点。仅由这两个节点构成的BST就是一棵初始的空BST。
void build()
{
get_node(-INF);//构建一个负无穷的节点
get_node(INF);//构建一个正无穷的节点
root=1;//根节点的下标为1号
tr[1].r=2;//根节点的右子节点的下标为2号
pushup(root);//由下往上更新节点信息
//如果右子节点的优先级大于根节点的优先级,则需要左旋
if(tr[1].val<tr[2].val)
zag(root);
}
创建新节点
int idx,root,INF=0x7fffffff;
int get_node(int key)
{
tr[++idx].key=key; //给这个新建的节点的关键码赋值为key
tr[idx].val=rand(); //给这个新建的节点随机生成一个优先级
tr[idx].cnt=1; //因为只新建了一个节点,因此该节点只有一个相同关键码
tr[idx].size=1; //因为只新建了一个系节点,那么以这个节点为根的子树就只有它本身,因此大小为1
return idx; //返回这个新建节点的数组下标
}
删除
用堆的方式删除
因为Treap满足堆性质,所以只需要把要删除的节点旋转到叶节点上,然后直接删除就可以了。核心思想:让优先级大的结点旋到上面,满足大根堆的性质
对于大根堆的具体方法如下:
- 如果该节点p的右子节点为空或者该节点p的左子节点的优先级大于右子节点的优先级,那么右旋该节点p,使该节点p降为右子树的根节点,然后访问右子树的根节点,继续操作;
- 如果该节点p的左子节点为空或者该节点p的右子节点的优先级大于左子节点的优先级,那么左旋该节点p,使该节点p降为左子树的根节点,然后访问左子树的根节点,继续操作,直到变成可以直接删除的节点。
下面以小根堆为例子:
这里最后错了,6还没有到达叶子节点,需要再对6进行右旋(或者左旋),使其到达叶子节点,然后就可以把6直接删除了。
删除代码
//由下往上更新节点信息
void pushup(int p)
{
//tr[p].l是节点p的左子节点的下标 tr[tr[p].l].size是(以节点p的左子节点为根的子树)中节点个数的大小
//tr[p].r是节点p的右子节点的下标 tr[tr[p].r].size是(以节点p的右子节点为根的子树)中节点个数的大小
// tr[p].size是以节点p为根的子树中的总节点个数
tr[p].size=tr[tr[p].l].size+tr[tr[p].r].size+tr[p].cnt; //tr[p].cnt是节点p中的相同关键码的个数
}
void remove(int &p,int key)
{
if(p==0) //如果要删除的这个节点并不存在
return;
if(tr[p].key==key) //如果检索到了该节点的关键码等于我们想要删除的key值
{
if(tr[p].cnt>1)//该节点中有多个相同的关键码,按照题目要求,如果题目只要求删除多个相同关键码中的一个,那就删除一个即可
{
tr[p].cnt--; //题目只要求删除一个相同的关键码即可
pushup(p);
return;//回溯
}
//如果不是叶子节点,则要通过旋转,把该节点旋转到叶子节点
if(tr[p].l||tr[p].r)
{
//如果p节点的右子节点为空,或者p的左子节点的优先级大于p的右子节点的优先级
if(tr[p].r==0||tr[tr[p].l].val>tr[tr[p].r].val)
{
zig(p); //先右旋
//此时想要删除的这个节点p转换到了右子树,那就递归右子树,最终使得p节点到达叶子节点,再把它删除
remove(tr[p].r,key);
}
//如果p节点的左子节点为空,或者p的右子节点的优先级大于p的左子节点的优先级
else
{
zag(p);//先左旋
//此时想要删除的这个节点p转换到了左子树,那就递归左子树,最终使得p节点到达叶子节点,再把它删除
remove(tr[p].l,key);
}
}
//这里说明p已经是叶子节点了,那么直接删除它即可
else
p=0;
}
//如果待删除的key值小于当前p节点关键码,由二叉搜索树可知,此时的key值应该是在左子树,那么就递归去左子树中寻找待删除的key值
else if(tr[p].key>key)
remove(tr[p].l,key); //tr[p].l是p节点的左子节点的下标
//如果待删除的key值大于当前p节点关键码,由二叉搜索树可知,此时的key值应该是在右子树,那么就递归去右子树中寻找待删除的key值
else
remove(tr[p].r,key);//tr[p].r是p节点的右子节点的下标
pushup(p);//删除key值后,由下往上更新节点信息
}
寻找前驱
寻找key的前驱,指的是在BST中关键码严格小于key值的前提下,关键码最大的那个节点。
int get_pre(int p,int key)
{
//如果要检索的key并不存在,直接返回一个负无穷就好了
if(p==0)
return -INF;
//如果要检索的key值小于等于当前p节点的关键码,则说明key在左子树,去左子树递归就好了
if(tr[p].key >= key)
return get_pre(tr[p].l,key);
//如果要检索的key值大于当前p节点的关键码,则说明key在右子树,去右子树递归就好了
//由二叉搜索树性质可知,对它进行中序遍历,得到的是单调递增的序列,那么左子树<根<右子树。由于上面的条件不满足,因此现在我们知道想要查找的key是在右子树,那么此时有两种情况
//比key值小的关键码最大的那个节点有可能是在右子树中,也有可能是根节点p,不可能是在左子树中了,因为左子树<根,如果取左子树,那么此时它就不是比key值小的最大关键码(先取到左子树才轮得到根)
//因此,我们要对根节点p和右子树进行取max
else
return max(tr[p].key,get_pre(tr[p].r,key));
}
寻找后继
寻找key的后继,指的是在BST中关键码严格大于key值的前提下,关键码最小的那个节点。
int get_next(int p,int key)
{
//如果要检索的key并不存在,直接返回一个正无穷就好了
if(p==0)
return INF;
//如果要检索的key值大于等于当前p节点的关键码,则说明key在右子树,去右子树递归就好了
if(tr[p].key <= key)
return get_next(tr[p].r,key);
//如果要检索的key值小于当前p节点的关键码,则说明key在左子树,去左子树递归就好了
//由二叉搜索树性质可知,对它进行中序遍历,得到的是单调递增的序列,那么左子树<根<右子树。由于上面的条件不满足,因此现在我们知道想要查找的key是在左子树,那么此时有两种情况
//比key值大的关键码最小的那个节点有可能是在左子树中,也有可能是根节点p,不可能是在右子树中了,因为根<右子树,如果取右子树,那么此时它就不是比key值大的最小关键码(先取到根才轮得到右子树)
//因此,我们要对根节点p和左子树进行取min
else
return min(tr[p].key,get_next(tr[p].l,key));
}