全网最强剖析平衡树Treap | 万字长文爆肝平衡树Treap

Treap是二叉搜索树和堆的结合,通过随机优先级实现平衡。在插入和删除操作中,Treap利用旋转保持堆性质,确保操作时间复杂度为O(logn)。插入节点时,给节点随机优先级,根据堆性质调整结构;删除节点时,通过旋转使其到达叶子节点并删除。此外,Treap还支持查找前驱和后继节点。
摘要由CSDN通过智能技术生成

平衡树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];	//用数组模拟链表

插入

给节点随机分配一个优先级,先和二叉搜索树的插入一样,先把要插入的节点插入到一个叶子节点上,然后 跟维护堆一样,如果当前节点的优先级比根大就旋转,若当前节点是根的左儿子就右旋,如果当前节点是根的右儿子就左旋。左旋能使原来的根节点转移到右边,右旋能使原来的根节点转移到左边。

如下图所示:数字越小,表示优先级就越大。

img

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述


插入代码

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直接删除了。

img


删除代码

//由下往上更新节点信息
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));
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

卷心菜不卷Iris

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值