1. 程式人生 > >skip list跳躍表實現

skip list跳躍表實現

原文連結:http://www.ezlippi.com/blog/2014/12/skip-list.html

跳錶(skip List)是一種隨機化的資料結構,基於並聯的連結串列,實現簡單,插入、刪除、查詢的複雜度均為O(logN)。跳錶的具體定義,
跳錶是由William Pugh發明的,這位確實是個大牛,搞出一些很不錯的東西。簡單說來跳錶也是

連結串列的一種,只不過它在連結串列的基礎上增加了跳躍功能,正是這個跳躍的功能,使得在查詢元素時,跳錶能夠提供O(log n)的時間複雜

度。紅黑樹等這樣的平衡資料結構查詢的時間複雜度也是O(log n),並且相對於紅黑樹這樣的平衡二叉樹skiplist的優點是更好的支援並

發操作,但是要實現像紅黑樹這樣的資料結構並非易事,但是隻要你熟悉連結串列的基本操作,再加之對跳錶原理的理解,實現一個跳錶資料

結構就是一個很自然的事情了。

此外,跳錶在當前熱門的開源專案中也有很多應用,比如LevelDB的核心資料結構memtable是用跳錶實現的,redis的sorted set資料

結構也是有跳錶實現的。

skiplist主要思想

先從連結串列開始,如果是一個簡單的連結串列(不一定有序),那麼我們在連結串列中查詢一個元素X的話,需要將遍歷整個連結串列直到找到元素X為止。

現在我們考慮一個有序的連結串列:

從該有序表中搜索元素 {13, 39} ,需要比較的次數分別為 {3, 5},總共比較的次數為 3 + 5 = 8 次。我們想下有沒有更優的演算法? 我們想到了對於

有序陣列查詢問題我們可以使用二分查詢演算法,但對於有序連結串列卻不能使用二分查詢。這個時候我們在想下平衡樹,比如BST,他們都是通過把一些

節點取出來作為其節點下某種意義的索引,比如父節點一般大於左子節點而小於右子節點。因此這個時候我們想到類似二叉搜尋樹的做法把一些

節點提取出來,作為索引。得到如下結構:

在這個結構裡我們把{3, 18, 77}提取出來作為一級索引,這樣搜尋的時候就可以減少比較次數了,比如在搜尋39時僅比較了3次(通過比較3,18,39)。

當然我們還可以再從一級索引提取一些元素出來,作為二級索引,這樣更能加快元素搜尋。

這基本上就是跳錶的核心思想,其實是一種通過“空間來換取時間”的一個演算法,通過在每個節點中增加了向前的指標(即層),從而提升查詢的效率。

跳躍列表是按層建造的。底層是一個普通的有序連結串列。每個更高層都充當下面列表的「快速跑道」,這裡在層 i 中的元素按某個固定的概率 p (通常

為0.5或0.25)出現在層 i+1 中。平均起來,每個元素都在 1/(1-p) 個列表中出現, 而最高層的元素(通常是在跳躍列表前端的一個特殊的頭元素)

在 O(log1/p n) 個列表中出現。

SkipList基本資料結構及其實現

一個跳錶,應該具有以下特徵:

1,一個跳錶應該有幾個層(level)組成;

2,跳錶的第一層包含所有的元素;

3,每一層都是一個有序的連結串列;

4,如果元素x出現在第i層,則所有比i小的層都包含x;

5,每個節點包含key及其對應的value和一個指向同一層連結串列的下個節點的指標陣列

如圖所示。

跳錶基本資料結構

定義跳錶資料型別:

//跳錶結構  
typedef struct skip_list  
{  
    int level;// 層數  
    Node *head;//指向頭結點  
} skip_list;  

其中level是當前跳錶最大層數,head是指向跳錶的頭節點如上圖。

跳錶的每個節點的資料結構:

typedef struct node  
{  
    keyType key;// key值  
    valueType value;// value值  
    struct node *next[1];// 後繼指標陣列,柔性陣列 可實現結構體的變長  
} Node;  

對於這個結構體重點說說,struct node *next[1] 其實它是個柔性陣列,主要用於使結構體包含可變長欄位。我們可以通過如下方法得到包含可變

層數(n)的Node *型別的記憶體空間:

#define new_node(n)((Node*)malloc(sizeof(Node)+n*sizeof(Node*)))

通過上面我們可以根據層數n來申請指定大小的記憶體,從而節省了不必要的記憶體空間(比如固定大小的next陣列就會浪費大量的記憶體空間)。

跳錶節點的建立

// 建立節點  
Node *create_node(int level, keyType key, valueType val)  
{  
    Node *p=new_node(level);  
    if(!p)  
        return NULL;  
    p->key=key;  
    p->value=val;  
    return p;  
}  

跳錶的建立

列表的初始化需要初始化頭部,並使頭部每層(根據事先定義的MAX_LEVEL)指向末尾(NULL)

//建立跳躍表  
skip_list *create_sl()  
{  
    skip_list *sl=(skip_list*)malloc(sizeof(skip_list));//申請跳錶結構記憶體  
    if(NULL==sl)  
        return NULL;  

    sl->level=0;// 設定跳錶的層level,初始的層為0層(陣列從0開始)  

    Node *h=create_node(MAX_L-1, 0, 0);//建立頭結點  
    if(h==NULL)  
    {  
        free(sl);  
        return NULL;  
    }  
    sl->head = h;  
    int i;  
     // 將header的next陣列清空  
    for(i=0; i<MAX_L; ++i)  
    {  
        h->next[i] = NULL;  
    }  
    srand(time(0));  
    return sl;  
}  

跳錶插入操作

我們知道跳錶是一種隨機化資料結構,其隨機化體現在插入元素的時候元素所佔有的層數完全是隨機的,層數是通過隨機演算法產生的:

//插入元素的時候元素所佔有的層數完全是隨機演算法  
int randomLevel()  
{  
    int level=1;  
    while (rand()%2)  
        level++;  
    level=(MAX_L>level)? level:MAX_L;  
    return level;  
}  

相當與做一次丟硬幣的實驗,如果遇到正面(rand產生奇數),繼續丟,遇到反面,則停止,用實驗中丟硬幣的次數level作為元素佔有的層數。

顯然隨機變數 level 滿足引數為 p = 1/2 的幾何分佈,level 的期望值 E[level] = 1/p = 2. 就是說,各個元素的層數,期望值是 2 層。

由於跳錶資料結構整體上是有序的,所以在插入時,需要首先查詢到合適的位置,然後就是修改指標(和連結串列中操作類似),然後更新跳錶的

level變數。 跳錶的插入總結起來需要三步:

1:查詢到待插入位置, 每層跟新update陣列;

2:需要隨機產生一個層數;

3:從高層至下插入,與普通連結串列的插入完全相同;

比如插入key為25的節點,如下圖:

對於步驟1,我們需要對於每一層進行遍歷並儲存這一層中下降的節點(其後繼節點為NULL或者後繼節點的key大於等於要插入的key),如下圖,

節點中有白色星花標識的節點儲存到update陣列。

對於步驟2我們上面已經說明了是通過一個隨機演算法產生一個隨機的層數,但是當這個隨機產生的層數level大於當前跳錶的最大層數時,我們

此時需要更新當前跳錶最大層數到level之間的update內容,這時應該更新其內容為跳錶的頭節點head,想想為什麼這麼做,呵呵。然後就是更

新跳錶的最大層數。

對於步驟3就和普通連結串列插入一樣了,只不過現在是對每一層連結串列進行插入節點操作。最終的插入結果如圖所示,因為新插入key為25的節點level隨機

為4大於插入前的最大層數,所以此時跳錶的層數為4。

實現程式碼如下:

bool insert(skip_list *sl, keyType key, valueType val)  
{  
    Node *update[MAX_L];  
    Node *q=NULL,*p=sl->head;//q,p初始化  
    int i=sl->level-1;  
    /******************step1*******************/  
    //從最高層往下查詢需要插入的位置,並更新update  
    //即把降層節點指標儲存到update陣列  
    for( ; i>=0; --i)  
    {  
        while((q=p->next[i])&& q->key<key)  
            p=q;  
        update[i]=p;  
    }  
    if(q && q->key == key)//key已經存在的情況下  
    {  
        q->value = val;  
        return true;  
    }  
    /******************step2*******************/  
    //產生一個隨機層數level  
    int level = randomLevel();  
    //如果新生成的層數比跳錶的層數大  
    if(level>sl->level)  
    {  
        //在update陣列中將新新增的層指向header  
        for(i=sl->level; i<level; ++i)  
        {  
            update[i]=sl->head;  
        }  
        sl->level=level;  
    }  
    //printf("%d\n", sizeof(Node)+level*sizeof(Node*));  
    /******************step3*******************/  
    //新建一個待插入節點,一層一層插入  
    q=create_node(level, key, val);  
    if(!q)  
        return false;  

    //逐層更新節點的指標,和普通連結串列插入一樣  
    for(i=level-1; i>=0; --i)  
    {  
        q->next[i]=update[i]->next[i];  
            update[i]->next[i]=q;  
        }  
        return true;  
}  

跳錶刪除節點操作

刪除節點操作和插入差不多,找到每層需要刪除的位置,刪除時和操作普通連結串列完全一樣。不過需要注意的是,如果該節點的level是最大的,

則需要更新跳錶的level。實現程式碼如下:

bool erase(skip_list *sl, keyType key)  
{  
    Node *update[MAX_L];  
    Node *q=NULL, *p=sl->head;  
    int i = sl->level-1;  
    for(; i>=0; --i)  
    {  
        while((q=p->next[i]) && q->key < key)  
        {  
            p=q;  
        }  
        update[i]=p;  
    }  
    //判斷是否為待刪除的key  
    if(!q || (q&&q->key != key))  
        return false;  

    //逐層刪除與普通連結串列刪除一樣  
    for(i=sl->level-1; i>=0; --i)  
    {  
        if(update[i]->next[i]==q)//刪除節點  
        {  
            update[i]->next[i]=q->next[i];  
            //如果刪除的是最高層的節點,則level--  
            if(sl->head->next[i]==NULL)  
                sl->level--;  
        }  
    }  
    free(q);  
    q=NULL;  
    return true;  
}  

跳錶的查詢操作

跳錶的優點就是查詢比普通連結串列快,其實查詢操已經在插入、刪除操作中有所體現,程式碼如下:

valueType *search(skip_list *sl, keyType key)  
{  
    Node *q,*p=sl->head;  
    q=NULL;  
    int i=sl->level-1;  
    for(; i>=0; --i)  
    {  
        while((q=p->next[i]) && q->key<key)  
        {  
            p=q;  
        }  
        if(q && key==q->key)  
            return &(q->value);  
    }  
    return NULL;  
}  

跳錶的銷燬

上面分別介紹了跳錶的建立、節點插入、節點刪除,其中涉及了記憶體的動態分配,在使用完跳錶後別忘了釋放所申請的記憶體,不然會記憶體洩露的。

不多說了,程式碼如下:

// 釋放跳躍表  
void sl_free(skip_list *sl)  
{  
    if(!sl)  
        return;  

    Node *q=sl->head;  
    Node *next;  
    while(q)  
    {  
        next=q->next[0];  
        free(q);  
        q=next;  
    }  
    free(sl);  
}  

skiplist複雜度分析

skiplist分析如下圖

參考: