数据结构与算法:有序表(二):跳表

前言

被邀请赛和区域赛的平衡树踢死了,回来学平衡树了 T^T......

一、原理

1.建立

跳表的节点结构和链表非常相似,不同在于其拥有多层指针。重点是,跳表在分配指针的时候,不是以固定的方式分配的,还是采用随机化的方式分配多层指针。方法是,首先每个节点都有一级指针,之后考虑使用随机函数每次生成一个 0 到 1 之间的数字。若数字小于 0.5 就生成一个新的指针,否则就停止生成的过程。其中为了防止出现运气很好一直在生成的情况,考虑设置一个最大层数 maxL,一般都取 logn 的规模。

首先,考虑设置一个值为负无穷的初始节点,其每一层的指针都是满的,初始均指向 NULL。之后在加入的时候,若当前数为 x,那么就从高层往低层考虑,每次找小于 x 的最右节点。

对于 x=5 的新节点 2,其随机出了两层指针。从上往下考虑,在上四层中,由于 5 只有两层,所以都不需要插入。而到了第二层时,此时由于 5 有第二层的指针,所以就将其插到第二层。之后去往第一层,还是一样操作,最终初始节点 1 的第一二层指针就都指向 2 号节点。

在 x=20 的 3 号节点来到时,还是从上往下考虑,那么在前四层还是不需要插入。当来到第二层时,此时发现 1 号节点的第二层指针指向的 2 号节点的权值为 5,小于当前的 20,那么就向右移动到 2 号节点。此时由于 3 号节点依然没有第二层指针,所以就不插入。之后就顺着 2 号节点往下来到第一层指针,此时就可以插入了。

当 x=7 的 4 号节点来到时,此时由于其有四层指针,那么就需要插入 1 号节点的第三四层指针。之后在第二层,发现 2 号节点的 5 小于 7,所以让 2 号节点的第二层指针指向 4 号节点。之后顺着 2 号节点往下,再让其第一层的指针指向 4 号节点,4 号节点的第一层指针指向原本的 3 号节点即可。对于 x=15 的 5 号节点也是类似,就是在第四层发现 4 号节点的 7 小于 15,那么就来到 4 号节点。之后往下来到后面几层时,都在这后面插入即可。

2.复杂度

复杂度方面,由于是随机化的方式分配层数,所以可以认为节点的层数和权值无关。那么也就是说这个往下的阶梯是很均匀的,不会出现高层全是较小数,第一层全是较大数,从而导致每次都要来到第一层再往右走很多。又因为第一层的节点个数是 n,第二层的个数是 n/2,第三层的个数是 n/4。在这样的结构下,又因为是从大往下考虑,那么当在高层走过了一个节点,必然在第一层就会跳过很多个节点。具体证明太难了,可以这么感性地理解()

3.查询

在查询时,还是每次跳到小于 x 的最右位置,这样一直干到第一层时,由于每个节点都会有第一层,所以此时的节点必然就是答案。大于 x 和等于 x 只需要在这个的基础上,往右多走一步或者两步即可。而对于删除,还是每次跳到小于 x 的最右位置,如果下一个是 x 就删除然后往下即可。

对于查询 x 的排名的问题,不难想到需要在高层跳的时候维护出跨过了第一层的几个节点信息。那么就可以对每个指针再多维护跳过几个节点的信息,之后就是像查小于 x 一样跳,返回个数即可。查排名是 x 的时候也可以类似这么搞,就是每次看能否往后跳,能就减少再考虑即可。

二、题目------普通平衡树

cpp 复制代码
#include <bits/stdc++.h>
using namespace std;

/*   /\_/\
*   (= ._.)
*   / >  \>
*/

/*
*想好再写
*注意审题 注意特判
*不要红温 不要急躁 耐心一点
*WA了不要立马觉得是思路不对 先耐心找反例
*/

#define endl '\n'
#define dbg(x) cout<<#x<<endl;cout<<x<<endl;
#define vdbg(a) cout<<#a<<endl;for(auto x:a)cout<<x<<" ";cout<<endl;
#define YES cout<<"YES"<<endl;return ;
#define Yes cout<<"Yes"<<endl;return ;
#define NO cout<<"NO"<<endl;return ;
#define No cout<<"No"<<endl;return ;
#define popcount __builtin_popcount
using ll=long long;
using ld=long double;
using pii=pair<int,int>;
using pll=pair<ll,ll>;
const int INF=1e9;
const ll INFLL=1e18;
const int dx[]={-1,1,0,0};
const int dy[]={0,0,-1,1};
const int ddx[]={-2,-1,1,2,2,1,-1,-2};
const int ddy[]={1,2,2,1,-1,-2,-2,-1};

template<typename T>
struct Skip_List
{
private:

    int MAXL;
    vector<T>key;
    vector<int>count;
    vector<int>level;
    vector<vector<int>>next;
    vector<vector<int>>len;
    int cnt;

    mt19937 myrand;
    int rand(){
        return uniform_int_distribution<int>(1,10)(myrand);
    }

    int random(){
        int ans=1;
        while(rand()<=5)
        {
            ans++;
        }
        return min(ans,MAXL);
    }

    void addCount(int i,int h,int num){
        while(next[i][h]!=0&&key[next[i][h]]<num){
            i=next[i][h];
        }

        if(h==1){
            count[next[i][h]]++;
        }else{
            addCount(i,h-1,num);
        }
        len[i][h]++;
    }

    //return from i until j is inserted, cross how many number
    int addNode(int i,int h,int j){
        int rightCnt=0;
        while(next[i][h]!=0&&key[next[i][h]]<key[j]){
            rightCnt+=len[i][h];
            i=next[i][h];
        }

        if(h==1){
            next[j][h]=next[i][h];
            next[i][h]=j;
            len[j][h]=count[next[j][h]];
            len[i][h]=1;
            return rightCnt;
        }

        int downCnt=addNode(i,h-1,j);
        if(h>level[j]){
            len[i][h]++;
        }else{
            next[j][h]=next[i][h];
            next[i][h]=j;
            len[j][h]=len[i][h]-downCnt;
            len[i][h]=downCnt+1;
        }
        return downCnt+rightCnt;
    }

    void eraseCount(int i,int h,int num){
        while(next[i][h]!=0&&key[next[i][h]]<num){
            i=next[i][h];
        }

        if(h==1){
            count[next[i][h]]--;
        }else{
            eraseCount(i,h-1,num);
        }
        len[i][h]--;
    }

    void eraseNode(int i,int h,int j){
        if(h<1){
            return ;
        }

        while(next[i][h]!=0&&key[next[i][h]]<key[j]){
            i=next[i][h];
        }

        if(h>level[j]){
            len[i][h]--;
        }else{
            next[i][h]=next[j][h];
            len[i][h]+=len[j][h]-1;
        }
        eraseNode(i,h-1,j);
    }

    int small(int i,int h,int num){
        int rightCnt=0;
        while(next[i][h]!=0&&key[next[i][h]]<num){
            rightCnt+=len[i][h];
            i=next[i][h];
        }

        if(h==1){
            return rightCnt;
        }
        return rightCnt+small(i,h-1,num);
    }

    int index(int i,int h,int x){
        int rightCnt=0;
        while(next[i][h]!=0&&rightCnt+len[i][h]<x){
            rightCnt+=len[i][h];
            i=next[i][h];
        }

        if(h==1){
            return key[next[i][h]];
        }
        return index(i,h-1,x-rightCnt);
    }

    int pre(int i,int h,int num){
        while(next[i][h]!=0&&key[next[i][h]]<num){
            i=next[i][h];
        }
        
        if(h==1){
            return key[i];
        }
        return pre(i,h-1,num);
    }

    int post(int i,int h,int num){
        while(next[i][h]!=0&&key[next[i][h]]<num){
            i=next[i][h];
        }
        
        if(h==1){
            if(next[i][h]==0){
                return INF;
            }
            if(key[next[i][h]]>num){
                return key[next[i][h]];
            }
            i=next[i][h];
            if(next[i][h]==0){
                return INF;
            }
            return key[next[i][h]];
        }
        return post(i,h-1,num);
    }

public:
    Skip_List(int n):MAXL(__lg(n)),key(n),count(n),level(n),next(n,vector<int>(__lg(n)+1)),len(n,vector<int>(__lg(n)+1)),myrand(time(0)){
        cnt=1;
        key[cnt]=-INF;
        level[cnt]=MAXL;
    }

    int find(int i,int h,int num){
        while(next[i][h]!=0&&key[next[i][h]]<num){
            i=next[i][h];
        }

        if(h==1){
            if(next[i][h]!=0&&key[next[i][h]]==num){
                return next[i][h];
            }
            return 0;
        }
        return find(i,h-1,num);
    }

    void insert(int num){
        if(find(1,MAXL,num)!=0){
            addCount(1,MAXL,num);
        }else{
            key[++cnt]=num;
            count[cnt]=1;
            level[cnt]=random();
            addNode(1,MAXL,cnt);
        }
    }

    void erase(int num){
        int j=find(1,MAXL,num);
        if(j!=0){
            if(count[j]>1){
                eraseCount(1,MAXL,num);
            }else{
                eraseNode(1,MAXL,j);
            }
        }
    }

    int rank(int num){
        return small(1,MAXL,num)+1;
    }

    int index(int x){
        return index(1,MAXL,x);
    }

    int pre(int num){
        return pre(1,MAXL,num);
    }

    int post(int num){
        return post(1,MAXL,num);
    }
};

void solve()
{
    int q;
    cin>>q;

    Skip_List<int>lst(q+1);

    int op,x;
    while(q--)
    {
        cin>>op;
        if(op==1)
        {
            cin>>x;

            lst.insert(x);
        }
        else if(op==2)
        {
            cin>>x;

            lst.erase(x);
        }
        else if(op==3)
        {
            cin>>x;

            cout<<lst.rank(x)<<endl;
        }
        else if(op==4)
        {
            cin>>x;

            cout<<lst.index(x)<<endl;
        }
        else if(op==5)
        {
            cin>>x;

            cout<<lst.pre(x)<<endl;
        }
        else if(op==6)
        {
            cin>>x;

            cout<<lst.post(x)<<endl;
        }
    }
}

void init()
{
}

signed main()
{
    ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
    int t=1;
    //cin>>t;
    init();
    while(t--)
    {
        solve();    
    }
    return 0;
}

唯一的问题就是在插入和删除时同步更新指针跨过的节点个数。考虑特判插入和删除时是否会导致新节点的产生,若不会的话,那么就只需要更新词频。这个在每次从 i 号节点第 h 层往右跳,最终的节点必然就是小于 num 的最右节点。若这个节点右侧就连着目标节点,那么就直接增加这个指针的词频。否则,就说明目标节点在当前层没有指针,那么就说明目标节点必然在当前节点往右的指针中,所以还是增加词频即可。

对于删除一个节点的问题,若往右跳完发现目标节点的层数达不到,那么还是直接减少指针的词频即可。否则的话先断连,然后让当前节点指针的词频加上目标节点指针的词频即可。

之后最麻烦的就是在插入一个新节点的部分时,因为在新插入一个节点的时候,这个节点会把原来的指针分为两部分,此时就需要更新这两部分跨过的节点个数。可以发现,在往右跳完以后,若目标节点的层数达不到,那么还是可以直接增加词频。否则,此时就需要求出当前节点到新插入的节点之间的个数,新插入节点指针跨过的个数可以通过原本的个数和前者相减求出。

既然需要求出下层跨过的节点个数,考虑让递归函数返回这个下层的个数 downCnt。有了这个值,就可以每次快速更新了。所以在第一层需要返回的就是往右跳过的个数 rightCnt,之后每次返回当前层往右跳的个数和下层的个数即可。

总结

这玩意儿也太妙了!

END

相关推荐
赴生-2 小时前
C++进阶 异常
开发语言·c++
不好听6132 小时前
深入理解链表:线性数据结构的另一面
javascript·数据结构
x138702859572 小时前
c语言中srtlen(指针使用计算字符长度)、传值和传址调用
c语言·开发语言·算法·visual studio
海兰3 小时前
【实用程序】电商销售分析仪表盘 — 从零搭建一个AI参与的全栈数据洞察系统
人工智能·学习·算法
凡人叶枫3 小时前
Effective C++ 条款28:避免使用 handles 指向对象内部
linux·服务器·开发语言·c++·嵌入式开发
zwenqiyu3 小时前
P5283 [十二省联考 2019] 异或粽子题解
c++·学习·算法
wayz113 小时前
Momentum:TSI(真实强度指数)技术指标详解
算法·金融·数据分析·量化交易·特征工程
Queenie_Charlie3 小时前
哈夫曼树
数据结构·c++·哈夫曼树
万事大吉CC4 小时前
Python 笔试输入模板总结
python·算法