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級別,這在內存較寬裕的狀況下仍是能夠接受的。可是若是問題要求的鍵值範圍較大,或者根本就不是整數時,線段樹可就很難適應了。這時候咱們就不得不考慮伸展樹、跳躍表這類基於元素構造的數據結構。而從實際測試結果看,跳躍表的效率並不比伸展樹差。加上編程複雜度上的優點,跳躍表盡顯出其簡單高效的特色。

相關文章
相關標籤/搜索