SkipList 跳跃表

http://blog.csdn.net/likun_tech/article/details/7354306
php

http://www.cnblogs.com/zhuangli/articles/1275665.html
html

http://www.cnblogs.com/xuqiang/archive/2011/05/22/2053516.html
node

为何选择跳表

目前常用的平衡数据结构有:B树,红黑树,AVL树,Splay Tree, Treep等。算法


想象一下,给你一张草稿纸,一只笔,一个编辑器,你能当即实现一颗红黑树,或者AVL树编程

出来吗? 很难吧,这须要时间,要考虑不少细节,要参考一堆算法与数据结构之类的树,数组

还要参考网上的代码,至关麻烦。数据结构


用跳表吧,跳表是一种随机化的数据结构,目前开源软件 Redis 和 LevelDB 都有用到它,dom

它的效率和红黑树以及 AVL 树不相上下,但跳表的原理至关简单,只要你能熟练操做链表,编辑器

就能轻松实现一个 SkipList。ide


有序表的搜索

考虑一个有序表:


d5d03b36-abff-34ea-9c40-a1fbfb709a81.jpg


从该有序表中搜索元素 < 23, 43, 59 > ,须要比较的次数分别为 < 2, 4, 6 >,总共比较的次数

为 2 + 4 + 6 = 12 次。有没有优化的算法吗?  链表是有序的,但不能使用二分查找。相似二叉

搜索树,咱们把一些节点提取出来,做为索引。获得以下结构:


7c904c3f-1f39-31af-b8cd-b6de27a94061.jpg


这里咱们把 < 14, 34, 50, 72 > 提取出来做为一级索引,这样搜索的时候就能够减小比较次数了。

咱们还能够再从一级索引提取一些元素出来,做为二级索引,变成以下结构:


96983cb0-d60a-31da-953d-2dde4036ea6b.jpg


 这里元素很少,体现不出优点,若是元素足够多,这种索引结构就能体现出优点来了


跳表

下面的结构是就是跳表:

其中 -1 表示 INT_MIN, 链表的最小值,1 表示 INT_MAX,链表的最大值。


f4c149bd-d8ea-39ff-813f-93d809c90966.jpg


跳表具备以下性质:

(1) 由不少层结构组成

(2) 每一层都是一个有序的链表

(3) 最底层(Level 1)的链表包含全部元素

(4) 若是一个元素出如今 Level i 的链表中,则它在 Level i 之下的链表也都会出现。

(5) 每一个节点包含两个指针,一个指向同一链表中的下一个元素,一个指向下面一层的元素。


跳表的搜索


ec9fd643-f85c-3072-8634-60cfc88ab334.jpg


例子:查找元素 117

(1) 比较 21, 比 21 大,日后面找

(2) 比较 37,   比 37大,比链表最大值小,从 37 的下面一层开始找

(3) 比较 71,  比 71 大,比链表最大值小,从 71 的下面一层开始找

(4) 比较 85, 比 85 大,从后面找

(5) 比较 117, 等于 117, 找到了节点。


具体的搜索算法以下:


C代码  
/* 若是存在 x, 返回 x 所在的节点,
 * 不然返回 x 的后继节点 */
find(x) 
{
    p = top;
while (1) {
while (p->next->key < x)
            p = p->next;
if (p->down == NULL) 
return p->next;
        p = p->down;
    }
}



跳表的插入

先肯定该元素要占据的层数 K(采用丢硬币的方式,这彻底是随机的)

而后在 Level 1 ... Level K 各个层的链表都插入元素。

例子:插入 119, K = 2


bb72be16-6162-3fee-b680-311f25dd7c3a.jpg


若是 K 大于链表的层数,则要添加新的层。

例子:插入 119, K = 4


6eac083f-45d9-37f9-867f-0d709d9659d3.jpg


丢硬币决定 K

插入元素的时候,元素所占有的层数彻底是随机的,经过一下随机算法产生:


C代码
int random_level()
{
    K = 1;
while (random(0,1))
        K++;
return K;
}



至关与作一次丢硬币的实验,若是遇到正面,继续丢,遇到反面,则中止,

用实验中丢硬币的次数 K 做为元素占有的层数。显然随机变量 K 知足参数为 p = 1/2 的几何分布,

K 的指望值 E[K] = 1/p = 2. 就是说,各个元素的层数,指望值是 2 层。



跳表的高度。

n 个元素的跳表,每一个元素插入的时候都要作一次实验,用来决定元素占据的层数 K,

跳表的高度等于这 n 次实验中产生的最大 K,待续。。。


跳表的空间复杂度分析

根据上面的分析,每一个元素的指望高度为 2, 一个大小为 n 的跳表,其节点数目的

指望值是 2n。


跳表的删除

在各个层中找到包含 x 的节点,使用标准的 delete from list 方法删除该节点。

例子:删除 71


7bab9ad1-9f5a-37d0-bc38-89ee50d1bc0d.jpg


#include <stdio.h>
#include <stdlib.h>
#include <malloc.h>
typedefint key_t;
typedefint value_t;
typedefstruct node_t
{
    key_t key;
    value_t value;
struct node_t *forward[];
} node_t;
typedefstruct skiplist
{
int level;
int length;
    node_t *header;
} list_t;
#define MAX_LEVEL   16
#define SKIPLIST_P  0.25
node_t* slCreateNode(int level, key_t key, value_t value)
{
    node_t *n = (node_t *)malloc(sizeof(node_t) + level * sizeof(node_t*));
if(n == NULL) return NULL;
    n->key = key;
    n->value = value;
return n;
}
list_t* slCreate(void)
{
    list_t *l = (list_t *)malloc(sizeof(list_t));
int i = 0;
if(l == NULL) return NULL;
    l->length = 0;
    l->level = 0;
    l->header = slCreateNode(MAX_LEVEL - 1, 0, 0);
for(i = 0; i < MAX_LEVEL; i++)
    {
        l->header->forward[i] = NULL;
    }
return l;
}
int randomLevel(void)
{
int level = 1;
while ((rand()&0xFFFF) < (SKIPLIST_P * 0xFFFF))
        level += 1;
return (level<MAX_LEVEL) ? level : MAX_LEVEL;
}
value_t* slSearch(list_t *list, key_t key)
{
    node_t *p = list->header;
int i;
for(i = list->level - 1; i >= 0; i--)
    {
while(p->forward[i] && (p->forward[i]->key <= key)){
if(p->forward[i]->key == key){
return &p->forward[i]->value;
            }
            p = p->forward[i];
        }
    }
return NULL;
}
int slDelete(list_t *list, key_t key)
{
    node_t *update[MAX_LEVEL];
    node_t *p = list->header;
    node_t *last = NULL;
int i = 0;
for(i = list->level - 1; i >= 0; i--){
while((last = p->forward[i]) && (last->key < key)){
            p = last;
        }
        update[i] = p;
    }
if(last && last->key == key){
for(i = 0; i < list->level; i++){
if(update[i]->forward[i] == last){
                update[i]->forward[i] = last->forward[i];
            }
        }
        free(last);
for(i = list->level - 1; i >= 0; i--){
if(list->header->forward[i] == NULL){
                list->level--;
            }
        }
        list->length--;
    }else{
return -1;
    }
return 0;
}
int slInsert(list_t *list, key_t key, value_t value)
{
    node_t *update[MAX_LEVEL];
    node_t *p, *node = NULL;
int level, i;
    p = list->header;
for(i = list->level - 1; i >= 0; i--){
while((node = p->forward[i]) && (node->key < key)){
            p = node;
        }
        update[i] = p;
    }
if(node && node->key == key){
        node->value = value;
return 0;
    }
    level = randomLevel();
if (level > list->level)
    {
for(i = list->level; i < level; i++){
            update[i] = list->header;
        }
        list->level = level;
    }
    node = slCreateNode(level, key, value);
for(i = 0; i < level; i++){
        node->forward[i] = update[i]->forward[i];
        update[i]->forward[i] = node;
    }
    list->length++;
return 0;
}
int main(int argc, char **argv)
{
    list_t *list = slCreate();
    node_t *p = NULL;
    value_t *val = NULL;
//插入
for(int i = 1; i <= 15; i++){
        slInsert(list, i, i*10);
    }
//删除
if(slDelete(list, 12) == -1){
        printf("delete:not found\n");
    }else{
        printf("delete:delete success\n");
    }
//查找
    val = slSearch(list, 1);
if(val == NULL){
        printf("search:not found\n");
    }else{
        printf("search:%d\n", *val);
    }
//遍历
    p = list->header->forward[0];
for(int i = 0; i < list->length; i++){
        printf("%d,%d\n", p->key, p->value);
        p = p->forward[0];
    }
    getchar();
return 0;
}




http://www.cxphp.com/?p=234(Redis中c语言的实现)

http://imtinx.iteye.com/blog/1291165

http://kenby.iteye.com/blog/1187303

http://bbs.bccn.net/thread-228556-1-1.html

http://blog.csdn.net/xuqianghit/article/details/6948554(leveldb源码)



二叉树是咱们都很是熟悉的一种数据结构。它支持包括查找、插入、删除等一系列的操做。但它有一个致命的弱点,就是当数据的随机性不够时,会致使其树型结构的不平衡,从而直接影响到算法的效率。

跳跃表(Skip List)是1987年才诞生的一种崭新的数据结构,它在进行查找、插入、删除等操做时的指望时间复杂度均为O(logn),有着近乎替代平衡树的本领。并且最重要的一点,就是它的编程复杂度较同类的AVL树,红黑树等要低得多,这使得其不管是在理解仍是在推广性上,都有着十分明显的优点。


跳跃表由多条链构成(S0,S1,S2 ……,Sh),且知足以下三个条件:

(1)每条链必须包含两个特殊元素:+∞ 和 -∞

(2)S0包含全部的元素,而且全部链中的元素按照升序排列。

(3)每条链中的元素集合必须包含于序数较小的链的元素集合,即:



基本操做


在对跳跃表有一个初步的认识之后,咱们来看一下基于它的几个最基本的操做


1、查找

目的:在跳跃表中查找一个元素x

在跳跃表中查找一个元素x,按照以下几个步骤进行:

i)从最上层的链(Sh)的开头开始

ii)假设当前位置为p,它向右指向的节点为q(p与q不必定相邻),且q的值为y。将y与x做比较

(1) x=y     输出查询成功及相关信息

(2) x>y     从p向右移动到q的位置

(3) x<y     从p向下移动一格


iii)    若是当前位置在最底层的链中(S0),且还要往下移动的话,则输出查询失败

2、插入

目的:向跳跃表中插入一个元素x

首先明确,向跳跃表中插入一个元素,至关于在表中插入一列从S0中某一位置出发向上的连续一段元素。有两个参数须要肯定,即插入列的位置以及它的“高度”。

关于插入的位置,咱们先利用跳跃表的查找功能,找到比x小的最大的数y。根据跳跃表中全部链均是递增序列的原则,x必然就插在y的后面。

而插入列的“高度”较前者来讲显得更加剧要,也更加难以肯定。因为它的不肯定性,使得不一样的决策可能会致使大相径庭的算法效率。为了使插入数据以后,保持该数据结构进行各类操做均为O(logn)复杂度的性质,咱们引入随机化算法(Randomized Algorithms)。

咱们定义一个随机决策模块,它的大体内容以下:

·产生一个0到1的随机数r                   r ← random()

·若是r小于一个常数p,则执行方案A,       if  r<p then do A

不然,执行方案B                                   else do B


初始时列高为1。插入元素时,不停地执行随机决策模块。若是要求执行的是A操做,则将列的高度加1,而且继续反复执行随机决策模块。直到第i次,模块要求执行的是B操做,咱们结束决策,并向跳跃表中插入一个高度为i的列。

性质1:    根据上述决策方法,该列的高度大于等于k的几率为pk-1。

此处有一个地方须要注意,若是获得的i比当前跳跃表的高度h还要大的话,则须要增长新的链,使得跳跃表仍知足先前所提到的条件。

咱们来看一个例子:

假设当前咱们要插入元素“40”,且在执行了随机决策模块后获得高度为4

·步骤一:找到表中比40小的最大的数,肯定插入位置



·步骤二:插入高度为4的列,并维护跳跃表的结构


3、删除

目的:从跳跃表中删除一个元素x

删除操做分为如下三个步骤:

(1)在跳跃表中查找到这个元素的位置,若是未找到,则退出     *

(2)将该元素所在整列从表中删除                              *

(3)将多余的“空链”删除                                    *

所谓“记忆化”查找,就是在前一次查找的基础上进行进一步的查找。它能够利用前一次查找所获得的信息,取其中能够被当前查找所利用的部分。利用“记忆化”查找能够将一次查找的复杂度变为O(logk),其中k为这次与前一次两个被查找元素在跳跃表中位置的距离。

下面来看一下记忆化搜索的具体实现方法:

假设上一次操做咱们查询的元素为i,这次操做咱们欲查询的元素为j。咱们用一个update数组来记录在查找i时,指针在每一层所“跳”到的最右边的位置。如图4.1中橘×××的元素。(蓝色为路径上的其它元素)


在插入元素j时,分为两种状况:

(1)i<=j

从S0层开始向上遍历update数组中的元素,直到找到某个元素,它向右指向的元素大于等于j,并于此处开始新一轮对j的查找(与通常的查找过程相同)

(2)i>j

从S0层开始向上遍历update数组中的元素,直到找到某个元素小于等于j,并于此处开始新一轮对j的查找(与通常的查找过程相同)


图4.2十分详细地说明了在查找了i=37以后,继续查找j=15或53时的两种不一样状况。






复杂度分析

一个数据结构的好坏大部分取决于它自身的空间复杂度以及基于它一系列操做的时间复杂度。跳跃表之因此被誉为几乎可以代替平衡树,其复杂度方面天然不会落后。咱们来看一下跳跃表的相关复杂度:


空间复杂度: O(n)           (指望)

跳跃表高度: O(logn)        (指望)

相关操做的时间复杂度:

查找: O(logn)        (指望)

插入:  O(logn)        (指望)

删除: O(logn)        (指望)


之因此在每一项后面都加一个“指望”,是由于跳跃表的复杂度分析是基于几率论的。有可能会产生最坏状况,不过这种几率极其微小。

下面咱们来一项一项分析。





1、 空间复杂度分析 O(n)

假设一共有n个元素。根据性质1,每一个元素插入到第i层(Si)的几率为pi-1 ,则在第i层插入的指望元素个数为npi-1,跳跃表的元素指望个数为 ,当p取小于0.5的数时,次数总和小于2n。

因此总的空间复杂度为O(n)


2、跳跃表高度分析 O(logn)

根据性质1,每一个元素插入到第i层(Si)的几率为pi ,则在第i层插入的指望元素个数为npi-1。

考虑一个特殊的层:第1+ 层。

层的元素指望个数为  = 1/n2,当n取较大数时,这个式子的值接近0,故跳跃表的高度为O(logn)级别的。


3、查找的时间复杂度分析 O(logn)

咱们采用逆向分析的方法。假设咱们如今在目标节点,想要走到跳跃表最左上方的开始节点。这条路径的长度,便可理解为查找的时间复杂度。

设当前在第i层第j列那个节点上。

i)若是第j列刚好只有i层(对应插入这个元素时第i次调用随机化模块时所产生的B决策,几率为1-p),则当前这个位置必然是从左方的某个节点向右跳过来的。

ii)若是第j列的层数大于i(对应插入这个元素时第i次调用随机化模块时所产生的A决策,几率为p),则当前这个位置必然是从上方跳下来的。(不可能从左方来,不然在之前就已经跳到当前节点上方的节点了,不会跳到当前节点左方的节点)

设C(k)为向上跳k层的指望步数(包括横向跳跃)

有:

C(0) = 0

C(k) = (1-p)(1+向左跳跃以后的步数)+p(1+向上跳跃以后的步数)

    = (1-p)(1+C(k)) + p(1+C(k-1))

C(k) = 1/p + C(k-1)

C(k) = k/p

而跳跃表的高度又是logn级别的,故查找的复杂度也为logn级别。


对于记忆化查找(Search with fingers)技术咱们能够采用相似的方法分析,很容易得出它的复杂度是O(logk)的(其中k为这次与前一次两个被查找元素在跳跃表中位置的距离)。


4、插入与删除的时间复杂度分析 O(logn)

插入和删除都由查找和更新两部分构成。查找的时间复杂度为O(logn),更新部分的复杂度又与跳跃表的高度成正比,即也为O(logn)。

因此,插入和删除操做的时间复杂度都为O(logn)


5、实际测试效果

(1)不一样的p对算法复杂度的影响



P


平均操做时间


平均列高


总结点数

每次查找跳跃次数

(平均值)

每次插入跳跃次数

(平均值)

每次删除跳跃次数

(平均值)

2/3

0.0024690 ms

3.004

91233

39.878

41.604

41.566

1/2

0.0020180 ms

1.995

60683

27.807

29.947

29.072

1/e

0.0019870 ms

1.584

47570

27.332

28.238

28.452

1/4

0.0021720 ms

1.330

40478

28.726

29.472

29.664

1/8

0.0026880 ms

1.144

34420

35.147

35.821

36.007

表1   进行106次随机操做后的统计结果


从表1中可见,当p取1/2和1/e的时候,时间效率比较高(为何?)。而若是在实际应用中空间要求很严格的话,那就能够考虑取稍小一些的p,如1/4。


(2)运用“记忆化”查找 (Search with fingers) 的效果分析

所谓“记忆化”查找,就是在前一次查找的基础上进行进一步的查找。它能够利用前一次查找所获得的信息,取其中能够被当前查找所利用的部分。利用“记忆化”查找能够将一次查找的复杂度变为O(logk),其中k为这次与前一次两个被查找元素在跳跃表中位置的距离。


P


数据类型

平均操做时间(不运用记忆化查找)

平均操做时间(运用记忆化查找)

平均每次查找跳跃次数(不运用记忆化查找)

平均每次查找跳跃次数(运用记忆化查找)


0.5

随机(相邻被查找元素键值差的绝对值较大)


0.0020150 ms


0.0020790 ms


23.262


26.509


0.5

先后具有相关性(相邻被查找元素键值差的绝对值较小)


0.0008440 ms


0.0006880 ms


26.157


4.932

表1   进行106次相关操做后的统计结果

从表2中可见,当数据相邻被查找元素键值差绝对值较小的时候,咱们运用“记忆化”查找的优点是很明显的,不过当数据随机化程度比较高的时候,“记忆化”查找不但不能提升效率,反而会由于跳跃次数过多而成为算法的瓶颈。

合理地利用此项优化,能够在特定的状况下将算法效率提高一个层次。



跳跃表的应用

高效率的相关操做和较低的编程复杂度使得跳跃表在实际应用中的范围十分普遍。尤为在那些编程时间特别紧张的状况下,高性价比的跳跃表极可能会成为你的得力助手。

能运用到跳跃表的地方不少,与其去翻陈年老题,不如来个趁热打铁,拿NOI2004第一试的第一题——郁闷的出纳员(Cashier)来“小试牛刀”吧。


例题一:NOI2004 Day1 郁闷的出纳员(Cashier)

[点击查看附录中的原题]

这道题解法的多样性给了咱们一次对比的机会。用不一样的算法和数据结构,在效率上会有怎样的差别呢?

首先定义几个变量

   R – 工资的范围

   N – 员工总数


咱们来看一下每一种适用的算法和数据结构的简要描述和理论复杂度:

(1)线段树

简要描述:以工资为关键字构造线段树,并完成相关操做。

I命令时间复杂度:O(logR)

A命令时间复杂度:O(1)

S命令时间复杂度:O(logR)

F命令时间复杂度:O(logR)

(2)伸展树(Splay tree)

简要描述:以工资为关键字构造伸展树,并经过“旋转”完成相关操做。

I命令时间复杂度:O(logN)

A命令时间复杂度:O(1)

S命令时间复杂度:O(logN)

F命令时间复杂度:O(logN)

(3)跳跃表(Skip List)

简要描述:运用跳跃表数据结构完成相关操做。

I命令时间复杂度:O(logN)

A命令时间复杂度:O(1)

S命令时间复杂度:O(logN)

F命令时间复杂度:O(logN)


实际效果评测: (单位:秒)



Test1

Test2

Test3

Test4

Test5

Test6

Test7

Test8

Test9

Test10

线段树

0.000

0.000

0.000

0.031

0.062

0.094

0.109

0.203

0.265

0.250

伸展树

0.000

0.000

0.016

0.062

0.047

0.125

0.141

0.360

0.453

0.422

跳跃表

0.000

0.000

0.000

0.047

0.062

0.109

0.156

0.368

0.438

0.375


从结果来看,线段树这种经典的数据结构彷佛占据着很大的优点。可有一点万万不能忽略,那就是线段树是基于键值构造的,它受到键值范围的约束。在本题中R的范围只有105级别,这在内存较宽裕的状况下仍是能够接受的。可是若是问题要求的键值范围较大,或者根本就不是整数时,线段树可就很难适应了。这时候咱们就不得不考虑伸展树、跳跃表这类基于元素构造的数据结构。而从实际测试结果看,跳跃表的效率并不比伸展树差。加上编程复杂度上的优点,跳跃表尽显出其简单高效的特色。

相关文章
相关标签/搜索