2023.3.10 【数据结构】普通平衡树

2023.3.10 【模板】普通平衡树

推荐一篇写平衡树写的很好的博客:算法学习笔记(18): 平衡树(一) - jeefy - 博客园 (cnblogs.com)

问题陈述

写一种数据结构,支持以下六种操作:
1.插入一个数x

2.删除一个数x

3.查询x的排名(比x小的数 + 1)

4.查询排名为x的数

5.查询x的前驱

6.查询x的后继

这种操作可以用一个叫二叉查找树(BST)的东西实现,这玩意有以下性质:

\[subtree(lson(x)) < x < subtree(rson(x)) \]

翻译过来,就是一个节点左子树的值小于这个节点,右子树的值大于这个节点

这样在理想状态下,就可以每次从树根开始,实现这个问题,设操作数为Q,则理论时间复杂度为\(O(Qlogn)\)

但是会有(奇葩的)数据,使得构造的树长成这个样子:

image

这样深度变成了\(O(n)\),时间复杂度就变成了\(O(n^2)\),无法满足题目要求。

基于这一点,我们可以考虑将这个不平衡的树变成平衡的(大体平衡的就行)。

总体上考虑随机算法。

以下将呈现三种平衡树:

Treap

Treap,意为Tree + Heap,字面理解,就是这棵树既具有堆的性质,又具有BST的性质。

如何做到呢?我们在每个节点上给它赋一个随机值Heap,依照Heap值的性质来进行操作。

我们可以发现,上面那棵树其实有好几种形态,而且都具有BST的性质:

image
image

可以观察到,这棵树像是一根悬挂在螺丝钉上的链子一样,可以往左拉,也可以往右拉。我们称之为”旋转“操作,这是平衡树最重要的操作之一。而”旋转“操作又分为”左旋“和”右旋“。

”左旋“ (Zag)

左旋,即”向左旋转“,即向左拉这条链子,具体些,我们使用一张图:

image

观察上图,我们发现有三个点的父、子关系发生了变化:1(grandfa)、3(fa)、4(grandson左) 和 5(son),(以下简称为gf,f,s,gs)

按照如下顺序改变关系:

1.f的右儿子 = gs

2.gs == 0 ? NULL : gs的父亲 = f

3.s的左儿子 = f

4.f的父亲 = s

5.gf == 0 ? 根 = s : gf相应方向的儿子 = s

6.s的父亲 = gf

改变后不要忘记维护值

至于那个”相应方向的儿子“,可以用一个get()函数来维护

inline int get(int x)//0左1右 
{
    return x == t[t[x].fa].rc;
}
t[f].rc = t[s].lc;
t[t[s].lc].fa = f;
t[s].lc = f;
if(gf != 0)
    (get(f) ? t[gf].rc : t[gf].lc) = s;
else
    root = s;
t[s].fa = gf;
t[f].fa = s;
maintain(x);
maintain(y);
if(z)
    maintain(z);

右旋(Zig)

同理,只是将方向改变一下即可:(以下gs为grandson右)

1.f的左儿子 = gs

2.gs == 0 ? NULL : gs的父亲 = f

3.s的右儿子 = f

4.f的父亲 = s

5.gf == 0 ? 根 = s : gf相应方向的儿子 = s

6.s的父亲 = gf

t[f].lc = t[s].rc;
t[t[s].rc].fa = f;
t[s].rc= f;
if(gf != 0)
    (get(f) ? t[gf].rc : t[gf].lc) = s;
else
    root = s;
t[s].fa = gf;
t[f].fa = s;
maintain(x);
maintain(y);
if(z)
    maintain(z);

为了方便,以上两个函数可以合并成一个函数rorate()

插入

首先按照BST的性质造点,在判断如果子节点的Heap值小于父节点的Heap值,就旋转子节点

删除

将这个点的两个儿子中Heap值小的旋转上来,直到这个点成为叶节点,删除这个点(直接断开它的father与它的联系即可)。注意删除后要沿着父亲这条链一路向上更新。

查找x的排名

直接找到比这个数小的数,然后+1即可

inline int Get_Rank_By_Val(int k,int x)
{
    if(x == 0) return 1;
    if(t[x].val == k) return t[t[x].lc].siz + 1;
    if(t[x].val < k) return t[x].cnt + t[t[x].lc].siz + Get_Rank_By_Val(k,t[x].rc);
    if(t[x].val > k) return Get_Rank_By_Val(k,t[x].lc);    
}

查找排名为x的数

如果当前排名在左子树大小 + 1 和 左子树大小 + 当前节点重复数 之间,就返回当前排名

inline int Get_Val_By_Rank(int k,int x)
{
    if(t[t[x].lc].siz + 1 <= k && k <= t[t[x].lc].siz + t[x].cnt) return t[x].val;
    if(k <= t[t[x].lc].siz)
        return Get_Val_By_Rank(k,t[x].lc);
    else
        return Get_Val_By_Rank(k - t[t[x].lc].siz - t[x].cnt,t[x].rc);
}

查找前驱

如果当前数大于等于k,就向左子树寻找,否则就记录下答案,然后找右子树即可

inline int Getpre(int k,int x)
{
    if(x == 0) return -0x7f7f7f7f;
    if(t[x].val >= k)
        return Getpre(k,t[x].lc);
    int maxi = -0x7f7f7f7f;
    if(t[x].val < k)
        maxi = max(maxi,Getpre(k,t[x].rc));
    maxi = max(maxi,t[x].val);
    return maxi;
}

查找后继

同理

inline int Getpost(int k,int x)
{
    if(x == 0) return 0x7f7f7f7f;
    if(t[x].val <= k)
        return Getpost(k,t[x].rc);
    int mini = 0x7f7f7f7f;
    if(t[x].val > k)
        mini = min(mini,Getpost(k,t[x].lc));
    mini = min(mini,t[x].val);
    return mini;
}

这样,我们就完成了Treap的编写。

Splay

Splay树最有特色的就是它的Splay函数,这个函数的作用是将当前节点一直转到根。通过将很多节点随机旋转到根的做法保持平衡,删除的时候还是将它旋转到叶节点

Splay完整代码:
#include<bits/stdc++.h>
using namespace std;
const int N = 1e5 + 5;
struct Splay_Tree{
    int lc,rc,siz,fa,val,cnt;
}t[N];
int n,root,tot = 0;
inline int maintain(int x)
{
    t[x].siz = t[t[x].lc].siz + t[t[x].rc].siz + t[x].cnt;
}
inline int get(int x)//0左1右 
{
    return x == t[t[x].fa].rc;
}
inline void rorate(int x)
{
    int y = t[x].fa,z = t[y].fa;
    if(!get(x))
    {
        t[y].lc = t[x].rc;
        t[t[x].rc].fa = y;
        t[x].rc = y;
        if(z != 0)
            (get(y) ? t[z].rc : t[z].lc) = x;
        else
            root = x;
        t[x].fa = z;
        t[y].fa = x;
    }
    else
    {
        t[y].rc = t[x].lc;
        t[t[x].lc].fa = y;
        t[x].lc = y;
        if(z != 0)
            (get(y) ? t[z].rc : t[z].lc) = x;
        else
            root = x;
        t[x].fa = z;
        t[y].fa = x;
    }
    maintain(y);
    maintain(x);
    if(z != 0)
        maintain(z); 
}
inline void splay(int x)
{
    while(t[x].fa != 0)
    {
        if(t[x].fa == root)
            rorate(x);
        else if(get(x) == get(t[x].fa)) 
            rorate(t[x].fa),rorate(x);
        else
            rorate(x),rorate(x);
    }
    root = x;
}
inline void upd_line(int x)
{
    maintain(x);
    if(t[x].fa != 0)
        upd_line(t[x].fa);
}
inline void insert(int k,int x)
{
    if(t[x].val == 0)
    {
        if(root == 0)
        {
            root = ++tot;
            x = root;
        }
        t[x].cnt++;
        t[x].val = k;
        upd_line(x);
        splay(x);
        return;
    }
    if(t[x].val == k)
    {
        ++t[x].cnt;
        upd_line(x);
        splay(x);
        return;
    }
    if(k < t[x].val) 
    {
        if(t[x].lc == 0)
        {
            t[x].lc = ++tot;
            t[t[x].lc].fa = x;
        }
        insert(k,t[x].lc);
    }
    else
    {
        if(t[x].rc == 0) 
        {
            t[x].rc = ++tot;
            t[t[x].rc].fa = x;
        }
        insert(k,t[x].rc);
    }
    maintain(x);
}
inline int Get_Num_By_Val(int k,int x)
{
    if(k == t[x].val) return x;
    if(k < t[x].val)
    {
        if(t[x].lc != 0)
             return Get_Num_By_Val(k,t[x].lc);
         else
             return x;
    }
    else 
    {
        if(t[x].rc != 0)
            return Get_Num_By_Val(k,t[x].rc);
        else
            return x;
    }
}
inline int Get_Rank_By_Val(int k,int x)
{
    if(x == 0) return 1;
    if(t[x].val == k) return t[t[x].lc].siz + 1;
    if(t[x].val < k) return t[x].cnt + t[t[x].lc].siz + Get_Rank_By_Val(k,t[x].rc);
    if(t[x].val > k) return Get_Rank_By_Val(k,t[x].lc);    
}
inline int Get_Val_By_Rank(int k,int x)
{
    if(t[t[x].lc].siz + 1 <= k && k <= t[t[x].lc].siz + t[x].cnt) return t[x].val;
    if(k <= t[t[x].lc].siz)
        return Get_Val_By_Rank(k,t[x].lc);
    else
        return Get_Val_By_Rank(k - t[t[x].lc].siz - t[x].cnt,t[x].rc);
}
inline int Getpre(int k,int x)
{
    if(x == 0) return -0x7f7f7f7f;
    if(t[x].val >= k)
        return Getpre(k,t[x].lc);
    int maxi = -0x7f7f7f7f;
    if(t[x].val < k)
        maxi = max(maxi,Getpre(k,t[x].rc));
    maxi = max(maxi,t[x].val);
    return maxi;
}
inline int Getpost(int k,int x)
{
    if(x == 0) return 0x7f7f7f7f;
    if(t[x].val <= k)
        return Getpost(k,t[x].rc);
    int mini = 0x7f7f7f7f;
    if(t[x].val > k)
        mini = min(mini,Getpost(k,t[x].lc));
    mini = min(mini,t[x].val);
    return mini;
}
inline void del(int k,int x)
{
    x = Get_Num_By_Val(k,root);
    if(x == -1) return;
    if(t[x].cnt > 1)
    {
        --t[x].cnt;
        upd_line(x);
        splay(x);
        return;
    }
    while(t[x].lc != 0 || t[x].rc != 0)
    {
        if(t[x].lc != 0)
            rorate(t[x].lc);
        else
            rorate(t[x].rc);
    }
    if(t[x].fa != 0)
    {
        (get(x) ? t[t[x].fa].rc : t[t[x].fa].lc) = 0;
        upd_line(t[x].fa);
    }
    else
        root = 0;
}
int main()
{
    cin>>n;
    int op,x;
    for(int i = 1;i <= n;i++)
    {
        cin>>op>>x;
        switch(op)
        {
            case 1:
                insert(x,root);break;
            case 2:
                del(x,root);break;
            case 3:
                cout<<Get_Rank_By_Val(x,root)<<endl;splay(Get_Num_By_Val(x,root));break;
            case 4:
                cout<<Get_Val_By_Rank(x,root)<<endl;break;
            case 5:
                cout<<Getpre(x,root)<<endl;splay(Get_Num_By_Val(x,root));break;
            case 6:
                cout<<Getpost(x,root)<<endl;splay(Get_Num_By_Val(x,root));break; 
        }
    }    
}

乍一看,含有”旋转“的平衡树写起来都稍显麻烦,那么有什么方法能让我们不旋转也能实现平衡树呢?

FHQ-Treap

详见《范浩强谈数据结构》一书

FHQ-Treap,属于非旋Treap之一。有两个核心操作:分裂(split) 和 合并(merge)。一般所采取的分裂是按Tree值分裂,而合并是按Tree和Heap两个值合并。

Split

image

(采用OI-Wiki上面的图)

我们采用引用调用,在每走到一个点时,如果这个点小于等于key的值,我们就将这个节点归为左树,然后去分割它的右子树,反之亦然。这样到叶节点时就会将这棵树彻底剖开。

同时我们就不用将Tree值相同的节点放在一个点里面,而是分为多个点就好了。

inline void split(int &x,int &y,int now,int k)
{
    if(!now)
    {
        x = y = 0;
        return;
    }
    if(t[now].val <= k)
        x = now,split(t[now].rc,y,t[now].rc,k);
    else
        y = now,split(x,t[now].lc,t[now].lc,k);
    update(now);
}

Merge

分裂后的两棵树有一个极好的性质:左子树一定小于右子树。那么在合并的时候,我们就可以充分利用这个性质。我们利用x树小于y树的性质,所以要么x树和y树的左子树合并,要么y树和x树的右子树合并,至于选哪个,就按照Heap值判断即可。

inline int merge(int x,int y)//默认 tree[x] < tree[y] 
{
    if(!x || !y) return x + y;
    if(t[x].hp < t[y].hp) 
    {
        t[x].rc = merge(t[x].rc,y);
        update(x);
        return x;
    }
    else
    {
        t[y].lc = merge(x,t[y].lc);
        update(y);
        return y;
    }
}

Insert

将整棵树按照x值分裂,将小树与x合并,再与大树合并即可。

inline int new_node(int x)
{
    ++tot;
    t[tot].lc = t[tot].rc = 0;
    t[tot].val = x;
    t[tot].hp = rand();
    t[tot].siz = 1;
    return tot;
}
inline void insert(int x)
{
    int y,z;
    split(y,z,root,x);
    root = merge(merge(y,new_node(x)),z);
}

Delete

将整棵树按照x值分裂,将小树按照x - 1分裂,为了防止等于x值的树(简称x树),将x定义为x的左儿子和右儿子合并即可,在分别将x - 1树,x树,大树合并。

inline void del(int x)
{
    int y,z,w,p;
    split(y,z,root,x);
    split(w,p,y,x - 1);
    p = merge(t[p].lc,t[p].rc);
    root = merge(merge(w,p),z);
}

求x的排名

将整棵树按照x - 1分裂,然后求小树的大小 + 1即可

inline int Get_Rank_By_Val(int x)
{
    int y,z;
    split(y,z,root,x - 1);
    int ret = t[y].siz + 1;
    root = merge(y,z);
    return ret;
}

求x的前驱

将整棵树按照x - 1分裂,然后取小树中的最大值即可

inline int Getpre(int x)
{
    int y,z;
    split(y,z,root,x - 1);
    int ret = t[kth(t[y].siz,y)].val;
    root = merge(y,z);
    return ret;
}

求x的后继

按照x + 1分裂,然后求大树中的最小值即可

inline int Getpost(int x)
{
    int y,z;
    split(y,z,root,x);
    int ret = t[kth(1,z)].val;
    root = merge(y,z);
    return ret;
}

Code

#include<bits/stdc++.h>
using namespace std;
const int N = 1e5 + 5;
struct fhq_treap{
    int lc,rc,val,hp,siz;
}t[N];
int root = 0,tot = 0;
inline void update(int pos)
{
    t[pos].siz = t[t[pos].lc].siz + t[t[pos].rc].siz + 1;
}
inline void split(int &x,int &y,int now,int k)
{
    if(!now)
    {
        x = y = 0;
        return;
    }
    if(t[now].val <= k)
        x = now,split(t[now].rc,y,t[now].rc,k);
    else
        y = now,split(x,t[now].lc,t[now].lc,k);
    update(now);
}
inline int merge(int x,int y)//默认 tree[x] < tree[y] 
{
    if(!x || !y) return x + y;
    if(t[x].hp < t[y].hp) 
    {
        t[x].rc = merge(t[x].rc,y);
        update(x);
        return x;
    }
    else
    {
        t[y].lc = merge(x,t[y].lc);
        update(y);
        return y;
    }
}
inline int new_node(int x)
{
    ++tot;
    t[tot].lc = t[tot].rc = 0;
    t[tot].val = x;
    t[tot].hp = rand();
    t[tot].siz = 1;
    return tot;
}
inline void insert(int x)
{
    int y,z;
    split(y,z,root,x);
    root = merge(merge(y,new_node(x)),z);
}
inline void del(int x)
{
    int y,z,w,p;
    split(y,z,root,x);
    split(w,p,y,x - 1);
    p = merge(t[p].lc,t[p].rc);
    root = merge(merge(w,p),z);
}
inline int Get_Rank_By_Val(int x)
{
    int y,z;
    split(y,z,root,x - 1);
    int ret = t[y].siz + 1;
    root = merge(y,z);
    return ret;
}
inline int kth(int x,int pos)
{
    if(x == t[t[pos].lc].siz + 1) return pos;
    else if (x > t[t[pos].lc].siz + 1) return kth(x - t[t[pos].lc].siz - 1,t[pos].rc);
    else return kth(x,t[pos].lc);
}
inline int Getpre(int x)
{
    int y,z;
    split(y,z,root,x - 1);
    int ret = t[kth(t[y].siz,y)].val;
    root = merge(y,z);
    return ret;
}
inline int Getpost(int x)
{
    int y,z;
    split(y,z,root,x);
    int ret = t[kth(1,z)].val;
    root = merge(y,z);
    return ret;
}
int main()
{
    srand(time(NULL));
    int n,opt,x;
    cin>>n;
    for(int i = 1;i <= n;i++)
    {
        cin>>opt>>x;
        switch(opt)
        {
            case 1:
                insert(x);break;
            case 2:
                del(x);break;
            case 3:
                cout<<Get_Rank_By_Val(x)<<endl;break;
            case 4:
                cout<<t[kth(x,root)].val<<endl;break;
            case 5:
                cout<<Getpre(x)<<endl;break;
            case 6:
                cout<<Getpost(x)<<endl;break;
        }
    }
    return 0;
}

完结撒花

posted @ 2023-03-11 07:46  The_Last_Candy  阅读(37)  评论(0编辑  收藏  举报