跳跃表的证明与实现

想写一个简单的东西来维护序列?

不想写红黑树来维护?

想口胡算法?

尝试跳跃表吧!

#引言

跳跃列表是在很多应用中有可能替代平衡树而作为实现方法的一种数据结构。跳跃列表的算法有同平衡树一样的渐进的预期时间边界,并且更简单、更快速和使用更少的空间。 -引自跳跃表的发明者William Pugh

跳跃表是一个十分新的算法,发明于1990年。相信大家都对打splay&treap的左旋,右旋感到厌烦吧,笔者第一次调Splay调了一天,还是在老师讲的情况下,十分头痛,代码量300+。但是,最近稍微看了看关于跳跃表的文章,就一下打出来了。好像插入25行,查找15行,删除25行,跑的与正常的平衡树没什么区别。下面开始正式介绍。

#1 跳跃表是个什么东东?

跳跃表其实起源于链表的优化。

首先,我们看看链表的格式:

\[{1 \xrightarrow{}{} 2 \xrightarrow{}{} 4\xrightarrow{}{} 5} \]

此时,我们让序列是有序的,以方便查找。

注意,此时我们已经有一个插入$$\Theta(n/2)$$的算法了。

提高效率?

\[\color{red}{1} \color{black}{\xrightarrow{}{} 2 \xrightarrow{}{} 4} \xrightarrow{}{} \color{red}{5} \color{black}{ \xrightarrow{}{} 6 \xrightarrow{}{} 7} \]

每一次先搜标红的点,找到刚好大于等与红标记但小于下一个红标记的点。

复杂度: $$\Theta(\text{红标记个数/2}+\text{总数/红标记个数/2})$$

当红标记个数等于总数开根时,$$ \Theta(\sqrt{n}) /2$$

然后,似乎红标记也可以进行同样的操作,复杂度可以在开个根。

然后继续对上一层标记操作,又可以开根…………

但是,到了最后,向下跳的层数会大于等于向右移的次数,我们就令最大的层数为k。

列出方程,即求

\[\min{(k + k (\log_{k}{n})/2)} \]

解得

\[k = \ln n \]

\[e^k = n \]

此时复杂度为

\[\Theta({\ln n}) \]

其实这里似乎咕了

#2 实现?

跳跃表性质


一个标准的跳跃表的一个链应该是这样子的:

注意! 很多关于跳跃表的文章都不会提到T,其实它是结构的一部分!!

可以看出,跳跃表中一个元素有两个指针:一个指向是左边的元素(nxt),一个指向下面的元素(down),还有一个值val,表示它代表的值。

##1 存储

struct node{
    int nxt;
    int val;
    int down;
    int jmp;//你可以先忽略这个值
}a[100000 * 4];

因为作者较懒用数组模拟链表。

是不是很简单啊

##2 初始化

inline void build(){
    for (register int i=1;i<=maxdep;i++){
        a[++al].nxt = maxdep + i;
        if (i != 1) a[al].dwn = al-1;
        else a[al].dwn = -1;
        a[al].jmp = 0;//你可以先不管
        a[al].val = -INF;
    }
    first = al;
    for (register int i=1;i<=maxdep;i++){
        a[++al].nxt = -1;
        if (i != -1) a[al].dwn = al-1;
        a[al].jmp = 100000;//你可以先不管
        a[al].val = INF;
    }
}

建图是很简单的。

这样就建成了一个高度为maxdep的图。

##3 查询

我先写查询的原因是会更加简单。

显然就如我所说,直接找就可以了。

inline pair <int,int> find_k_rank_and_p(int x)
{
    register int NOW = first;
    register int len = 0;
    register int dep = maxdep;
    while (dep > 0)
    {
        while (x > a[a[NOW].nxt].val)
            len += a[NOW].jmp, NOW = a[NOW].nxt;
        dep--;
        if (dep == 0) break;
        NOW = a[NOW].dwn;
    }
    return make_pair(len+1,NOW);
}

##4 插入

注意要插入时高度必须随机,并且以e的次数减少。

但是,因为插入是改动量太多,O(插入) > O(查询),建议取4。

inline int dfs_insert(int x,int NOW,int dep,int insdep){
    if (dep == 0) return 0;
    register int len_jmp = 0;
    while (x > a[a[NOW].nxt].val)
        len_jmp += a[NOW].jmp, NOW = a[NOW].nxt;
    register int lft_jmp = dfs_insert(x,a[NOW].dwn,dep-1,insdep);
    if (dep > insdep)
        a[NOW].jmp++;
    else{
        a[++al].nxt = a[NOW].nxt;
        a[NOW].nxt = al;
        a[al].dwn = al-1;
        a[al].val = x;
        a[al].jmp = a[NOW].jmp - lft_jmp;
        a[NOW].jmp = lft_jmp + 1;
    }
    return len_jmp + lft_jmp;
}

inline void insert(int x)
{
    register int insdep = 1;
    register int random = rand()<<16|rand();
    while (random&3) insdep++,random/=2;//***重要!!这一步最耗时间!!不要一直rand()!!
    dfs_insert(x,first,maxdep,insdep);
}

##删除

可以类比指针的删除。

inline void del(int x)
{
    register int rank = find_k_rank(x);
    register int NOW = first;
    register int len = 0;
    register int dep = maxdep;
    while (dep > 0)
    {
        while (x > a[a[NOW].nxt].val)
            len += a[NOW].jmp, NOW = a[NOW].nxt;
        if (a[a[NOW].nxt].val == x && len + a[NOW].jmp == rank) // be del
            a[NOW].jmp = a[NOW].jmp + a[a[NOW].nxt].jmp - 1,a[NOW].nxt = a[a[NOW].nxt].nxt;
        else
            a[NOW].jmp--;
        NOW = a[NOW].dwn;
        dep--;
    }
}

## 寻找rank

我们只要对数据多维护一个jmp即下一个与指针间隔了多少数据,即可维护rank。


inline int find_rank_val(int x)
{
    register int NOW = first;
    register int len = 0;
    register int dep = maxdep;
    while (dep > 0)
    {
        while (x > len + a[NOW].jmp)
            len += a[NOW].jmp, NOW = a[NOW].nxt;
        dep--;
        if (dep == 0)
            break;
        NOW = a[NOW].dwn;
    }
    return a[a[NOW].nxt].val;
}

## 代码

#include<bits/stdc++.h>
#define repeat(a,b,c,d) for (int a=b;a<=c;a+=d)
using namespace std;
struct node{
    int nxt,dwn,jmp,val;
}a[100000 * 4];
int al = 0,n,first;
const int maxdep = 9, INF = 1e9;
inline void build(){//在程序开始时调用,建造一个dep=maxdep的表
    for (register int i=1;i<=maxdep;i++){//建造开始节点
        a[++al].nxt = maxdep + i;//指向最后节点
        if (i != 1) a[al].dwn = al-1;//非第一个节点指向下一个down节点
        else a[al].dwn = -1;//最后一个
        a[al].jmp = 0;a[al].val = -INF;//赋值,a[al].jmp可以任意赋。
    }
    first = al;//指向第一个节点
    for (register int i=1;i<=maxdep;i++){
        a[++al].nxt = -1;//same
        if (i != -1) a[al].dwn = al-1;
        a[al].jmp = INF;a[al].val = INF;
    }
}
//之后我们维护NOW指针。
inline int dfs_insert(int x,int NOW,int dep,int insdep){//为了常数优化,不先调用find_k_rank_and_p找位置,而是直接递归位置。
    if (dep == 0) return 0;//边界
    register int len_jmp = 0;//我们从现在NOW指针往前蹦的长度
    while (x > a[a[NOW].nxt].val) len_jmp += a[NOW].jmp, NOW = a[NOW].nxt;//**记住,所有的skip list的搜索都是开区间的!**
    register int lft_jmp = dfs_insert(x,a[NOW].dwn,dep-1,insdep);//搜到下一层
    if (dep > insdep) a[NOW].jmp++;//如果没达到指定层数,只是NOW的jmp指针增加
    else{
        a[++al].nxt = a[NOW].nxt;a[NOW].nxt = al;a[al].dwn = al-1;a[al].val = x;//维护指针
        a[al].jmp = a[NOW].jmp - lft_jmp;a[NOW].jmp = lft_jmp + 1;//一并维护NOW
    }
    return len_jmp + lft_jmp;//返回跳过的层数。
}
inline void insert(int x){
    register int insdep = 1,random = rand()<<16|rand();//**千万不要rand来rand去,贼慢!
    while (random%4 == 0) insdep++,random/=4;dfs_insert(x,first,maxdep,insdep);//亲测4层最快
}
inline pair<int,int> find_k_rank_and_p(int x){
    register int NOW = first,len = 0,dep = maxdep;//寻找x的rank并且返回NOW指针(全部开区间)
    while (dep > 0){
        while (x > a[a[NOW].nxt].val) len += a[NOW].jmp, NOW = a[NOW].nxt;//能走则走
        dep--;//维护层数
        if (dep == 0) break;//为了维护NOW,一旦dep==0就break;
        NOW = a[NOW].dwn;
    }
    return make_pair(len+1,NOW);//len是开区间,要+1
}
inline void del(int x){
    register int rank = find_k_rank_and_p(x).first,NOW = first,len = 0,dep = maxdep;//删除时先找出要删的位置,必须保证不多删。
    while (dep > 0){
        while (x > a[a[NOW].nxt].val) len += a[NOW].jmp, NOW = a[NOW].nxt;//找
        if (a[a[NOW].nxt].val == x && len + a[NOW].jmp == rank) // be del
            a[NOW].jmp = a[NOW].jmp + a[a[NOW].nxt].jmp - 1,a[NOW].nxt = a[a[NOW].nxt].nxt;//维    护NOW指针
        else a[NOW].jmp--;
        NOW = a[NOW].dwn;
        dep--;
    }
}
inline int find_rank_val(int x){
    register int NOW = first,len = 0,dep = maxdep;
    while (dep > 0){
        while (x > len + a[NOW].jmp)
            len += a[NOW].jmp, NOW = a[NOW].nxt;//能蹦则蹦
        dep--;
        if (dep == 0) break;
        NOW = a[NOW].dwn;
    }
    return a[a[NOW].nxt].val;//返回值
}
int main(){
    srand(time(NULL));
    build();
    scanf("%d",&n);
    for (int i=1;i<=n;i++){
        register int opt,x;
        scanf("%d%d",&opt,&x);
        if (opt == 1) insert(x);//插入
        if (opt == 2) del(x);//删除
        if (opt == 3) printf("%d\n",find_k_rank_and_p(x).first);//查找x数位置
        if (opt == 4) printf("%d\n",find_rank_val(x));//查找rank为x的值
        if (opt == 5) printf("%d\n",a[find_k_rank_and_p(x).second].val);//求x的前驱
        if (opt == 6) printf("%d\n",a[a[find_k_rank_and_p(x+1).second].nxt].val);//求x的后缀
    }
}

posted @ 2019-07-10 20:22  dgklr  阅读(421)  评论(0编辑  收藏  举报