skip list

介绍

skip list是一个非常优秀的数据结构,插入、删除、查找的复杂度均为O(logN),相对于AVL和 红黑树,跳跃表的实现更加地简单。LevelDB的核心数据结构是用跳表实现。

skip list中各个操作的时间复杂度如下所示:

Operation	        | Time Complexity
-------------------------------------
Insertion	        | O(log N)
Removal	            | O(log N)
Check if contains   | O(log N)
Enumerate in order  | O(N)

盛大创新院的这篇文章对skip list作了很好的介绍, 建议没有接触过skip list的同学先看一下。既然网络上已经有不错的材料,我就不再浪费时间 做无用功了,本文仅介绍skip list的一个简单实现。

实现

skip list概念是其实很好理解,就是一个普通的链表,链表的每个节点高度不一,节点中更高 的链接指向更远的节点(站得高,看得远?)。在查找的时候,就可以首先通过较高的节点跳过那 些不可能的值,以此加快查找速度。

刚接触skip list,可能会有两个疑惑:

  1. 如何在各个节点中包含不同数量的链接,即如何实现?
  2. 在插入和删除节点时,可能会改变多个指针,如何做到?

当然,这也可能是我自己的两个疑惑,带着这两个问题,一起来分析一下skip list的实现。

下面来看第一个问题,如何实现在各个节点中包含不同数量的链接。skip list的初始化代码如下:

#define lgNMax 30

typedef struct STnode* link;
typedef int Item;
tpedef Item Key;

struct STnode{ Item item; link * next; int sz; }
static link head, z; //头结点和尾节点
static int N, lgN;   //节点总数和skip list当前的高度

link NEW(Item item, int k)
{
    int i; link x = malloc(*x);
    x->next = malloc(k * sizeof(link));
    x->item = item; x->sz = k;
    for (i = 0; i < k; i++) x->next[i] = z;
    return x;
}

void STinit(int max)
{
    N = 0; lg = 0;
    z = NEW(NULL, 0);
    head = NEW(NULL, lgNMax + 1)
}

如果只看节点定义,根本没有什么特别之处,这就是一个普通的链表节点的定义。 重点是NEW函数,分析函数代码可知,节点的next不是指向下一个节点,而是指向一个数组, 这个数组中,包含了k个元素, 其中,数组的第1个元素(如head->next[0]) 用以发挥了普通链表的作用,指向skip list中的 下一个节点。 数组的第2个元素到第k-1个元素,是skip list所特有的,用以跳过某些节点, 指向离自己更远的节点。

如下图所示:

skip

这就回答了上面两个问题的第一个问题。下面来看skip list的查找,因为skip list的查找比较简单, 所以先看查找操作。源码如下:

Item searchR(link k, Key v, int k)
{
    if(t == z)return NULL;
    if(v == t->item)return t->item;
    if(v < t->next[k]->item)
    {
        if(k == 0)return NULL;
        return searchR(t, v, k-1); //向下
    }
    return searchR(t->next[k], v, k); //向前
}

Item STsearch(Key v)
{ return searchR(head, v, lgN); }

最关键的代码就是有注释的两行代码,完整的步骤如下:

  1. 当前是否已经到达了skip list的尾部,如果是,则直接返回未找到
  2. 要找的key是否等于当前节点的值,如果等于,就返回找到
  3. 没有找到,就需要考虑两种情况。如果要找的key比当前节点小,则向下继续 查找searchR(t, v, k-1);如果已经到达底部(不能再向下),则返回未找到
  4. 如果要找的key大于当前节点的key,则向前查找searchR(t->next[k], v, k)

下面来看插入操作,请忽略randX()函数。

int randX()
{
    int i, j, t = rand();
    for ( i = 1, j = 2; i < lgNMax; i++, j += j)
        if ( t > RAND_MAX/j) break;
    if(i > lgN) lgN = i;
    return i;
}


void insertR(link t, link x, int k)
{
    Key v = x->item;
    if( v < t->next[k]->item)
    {
        if (k < x->sz)
        {
            x->next[k] = t->next[k];
            t->next[k] = x;
        }
        if ( k == 0 )return;
        insertR(t, x, k - 1)return; //向下
    }
    insertR(t, x, k-1); //向前
}

void STinsert(Item item)
{
    insertR(head, NEW(item, randX()), lgN);
    N++;
}

在绝大多数数据结构中,做插入操作,将节点插入以后就完事了,但是,前面说过,skip list 可能会修改多个指针。即,skip list中的插入操作,并不是插入以后就完事了, 还需继续向下进行指针的修改。插入过程其实与查找过程非常相似。

要牢记,skip list中的插入操作并不是插入一次以后就完成了,而是到达了节点的底部, 不能再“向下走”以后才结束。这就回答了上面两个问题中的第二个问题, 如何做到一次插入改变多个指针,且每次改变的指针数还不固定。

删除操作与插入操作非常类似,不再赘述。

void deleteR(link t, Key v, int	 k)
{
    l	ink x = t->next[k];
    i	f(x->item > v)
    {
     	   if(v == x-	>item)
     	   { t->next[	k] = x->next[	k]; }
     	   if(k == 0)
     	   { free(x); return; }
     	   deleteR(t, v, k - 1); r	eturn;
    }
    deleteR(t->next[k], v, k);
}

Item STdelete(Key v)
{ deleteR(head, v, lgN); N--; }

本文的skip list实现来自于《算法C语言实现》原书第三版13.5小节。

其他资料:

[1]: Skip lists are fascinating!非递归实现的跳跃表

[2]: leveldb中的skip list 工业界的实现

赖明星 /
Published under (CC) BY-NC-SA in categories 程序语言  tagged with 算法  skip list