skip跳躍表的實現

skiplist介紹

跳錶(skip List)是一種隨機化的數據結構,基於並聯的鏈表,實現簡單,插入、刪除、查找的複雜度均爲O(logN)。跳錶的具體定義,

 跳錶是由William Pugh發明的,這位確實是個大牛,搞出一些很不錯的東西。簡單說來跳錶也是html

鏈表的一種,只不過它在鏈表的基礎上增長了跳躍功能,正是這個跳躍的功能,使得在查找元素時,跳錶可以提供O(log n)的時間複雜前端

度。紅黑樹等這樣的平衡數據結構查找的時間複雜度也是O(log n),而且相對於紅黑樹這樣的平衡二叉樹skiplist的優勢是更好的支持並node

發操做,可是要實現像紅黑樹這樣的數據結構並不是易事,可是隻要你熟悉鏈表的基本操做,再加之對跳錶原理的理解,實現一個跳錶數據redis

結構就是一個很天然的事情了。算法

此外,跳錶在當前熱門的開源項目中也有不少應用,好比LevelDB的核心數據結構memtable是用跳錶實現的,redis的sorted set數據數組

結構也是有跳錶實現的。數據結構

 

skiplist主要思想

 

先從鏈表開始,若是是一個簡單的鏈表(不必定有序),那麼咱們在鏈表中查找一個元素X的話,須要將遍歷整個鏈表直到找到元素X爲止。dom

如今咱們考慮一個有序的鏈表:spa

                                             

 

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

有序數組查找問題咱們可使用二分查找算法,但對於有序鏈表卻不能使用二分查找。這個時候咱們在想下平衡樹,好比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和一個指向同一層鏈表的下個節點的指針數組

如圖所示。

                     

 

跳錶基本數據結構

定義跳錶數據類型:

[cpp]  view plain copy
 
  1. //跳錶結構  
  2. typedef struct skip_list  
  3. {  
  4.     int level;// 層數  
  5.     Node *head;//指向頭結點  
  6. } skip_list;  

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

跳錶的每一個節點的數據結構:

[cpp]  view plain copy
 
  1. typedef struct node  
  2. {  
  3.     keyType key;// key值  
  4.     valueType value;// value值  
  5.     struct node *next[1];// 後繼指針數組,柔性數組 可實現結構體的變長  
  6. } Node;  

對於這個結構體重點說說,struct node *next[1] 其實它是個柔性數組,主要用於使結構體包含可變長字段。咱們能夠經過以下方法獲得包含可變

層數(n)的Node *類型的內存空間:

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

 

經過上面咱們能夠根據層數n來申請指定大小的內存,從而節省了沒必要要的內存空間(好比固定大小的next數組就會浪費大量的內存空間)。

跳錶節點的建立

[cpp]  view plain copy
 
  1. // 建立節點  
  2. Node *create_node(int level, keyType key, valueType val)  
  3. {  
  4.     Node *p=new_node(level);  
  5.     if(!p)  
  6.         return NULL;  
  7.     p->key=key;  
  8.     p->value=val;  
  9.     return p;  
  10. }  

跳錶的建立

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

[cpp]  view plain copy
 
  1. //建立跳躍表  
  2. skip_list *create_sl()  
  3. {  
  4.     skip_list *sl=(skip_list*)malloc(sizeof(skip_list));//申請跳錶結構內存  
  5.     if(NULL==sl)  
  6.         return NULL;  
  7.   
  8.     sl->level=0;// 設置跳錶的層level,初始的層爲0層(數組從0開始)  
  9.   
  10.     Node *h=create_node(MAX_L-1, 0, 0);//建立頭結點  
  11.     if(h==NULL)  
  12.     {  
  13.         free(sl);  
  14.         return NULL;  
  15.     }  
  16.     sl->head = h;  
  17.     int i;  
  18.      // 將header的next數組清空  
  19.     for(i=0; i<MAX_L; ++i)  
  20.     {  
  21.         h->next[i] = NULL;  
  22.     }  
  23.     srand(time(0));  
  24.     return sl;  
  25. }  

跳錶插入操做

咱們知道跳錶是一種隨機化數據結構,其隨機化體如今插入元素的時候元素所佔有的層數徹底是隨機的,層數是經過隨機算法產生的:

[cpp]  view plain copy
 
  1. //插入元素的時候元素所佔有的層數徹底是隨機算法  
  2. int randomLevel()  
  3. {  
  4.     int level=1;  
  5.     while (rand()%2)  
  6.         level++;  
  7.     level=(MAX_L>level)? level:MAX_L;  
  8.     return level;  
  9. }  

至關與作一次丟硬幣的實驗,若是遇到正面(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。

                 

 

  實現代碼以下:

[cpp]  view plain copy
 
  1. bool insert(skip_list *sl, keyType key, valueType val)  
  2. {  
  3.     Node *update[MAX_L];  
  4.     Node *q=NULL,*p=sl->head;//q,p初始化  
  5.     int i=sl->level-1;  
  6.     /******************step1*******************/  
  7.     //從最高層往下查找須要插入的位置,並更新update  
  8.     //即把降層節點指針保存到update數組  
  9.     for( ; i>=0; --i)  
  10.     {  
  11.         while((q=p->next[i])&& q->key<key)  
  12.             p=q;  
  13.         update[i]=p;  
  14.     }  
  15.     if(q && q->key == key)//key已經存在的狀況下  
  16.     {  
  17.         q->value = val;  
  18.         return true;  
  19.     }  
  20.     /******************step2*******************/  
  21.     //產生一個隨機層數level  
  22.     int level = randomLevel();  
  23.     //若是新生成的層數比跳錶的層數大  
  24.     if(level>sl->level)  
  25.     {  
  26.         //在update數組中將新添加的層指向header  
  27.         for(i=sl->level; i<level; ++i)  
  28.         {  
  29.             update[i]=sl->head;  
  30.         }  
  31.         sl->level=level;  
  32.     }  
  33.     //printf("%d\n", sizeof(Node)+level*sizeof(Node*));  
  34.     /******************step3*******************/  
  35.     //新建一個待插入節點,一層一層插入  
  36.     q=create_node(level, key, val);  
  37.     if(!q)  
  38.         return false;  
  39.   
  40.     //逐層更新節點的指針,和普通鏈表插入同樣  
  41.     for(i=level-1; i>=0; --i)  
  42.     {  
  43.         q->next[i]=update[i]->next[i];  
  44.         update[i]->next[i]=q;  
  45.     }  
  46.     return true;  
  47. }  

跳錶刪除節點操做

刪除節點操做和插入差很少,找到每層須要刪除的位置,刪除時和操做普通鏈表徹底同樣。不過須要注意的是,若是該節點的level是最大的,

則須要更新跳錶的level。實現代碼以下:

[cpp]  view plain copy
 
  1. bool erase(skip_list *sl, keyType key)  
  2. {  
  3.     Node *update[MAX_L];  
  4.     Node *q=NULL, *p=sl->head;  
  5.     int i = sl->level-1;  
  6.     for(; i>=0; --i)  
  7.     {  
  8.         while((q=p->next[i]) && q->key < key)  
  9.         {  
  10.             p=q;  
  11.         }  
  12.         update[i]=p;  
  13.     }  
  14.     //判斷是否爲待刪除的key  
  15.     if(!q || (q&&q->key != key))  
  16.         return false;  
  17.   
  18.     //逐層刪除與普通鏈表刪除同樣  
  19.     for(i=sl->level-1; i>=0; --i)  
  20.     {  
  21.         if(update[i]->next[i]==q)//刪除節點  
  22.         {  
  23.             update[i]->next[i]=q->next[i];  
  24.             //若是刪除的是最高層的節點,則level--  
  25.             if(sl->head->next[i]==NULL)  
  26.                 sl->level--;  
  27.         }  
  28.     }  
  29.     free(q);  
  30.     q=NULL;  
  31.     return true;  
  32. }  

跳錶的查找操做

跳錶的優勢就是查找比普通鏈表快,其實查找操已經在插入、刪除操做中有所體現,代碼以下:

[cpp]  view plain copy
 
  1. valueType *search(skip_list *sl, keyType key)  
  2. {  
  3.     Node *q,*p=sl->head;  
  4.     q=NULL;  
  5.     int i=sl->level-1;  
  6.     for(; i>=0; --i)  
  7.     {  
  8.         while((q=p->next[i]) && q->key<key)  
  9.         {  
  10.             p=q;  
  11.         }  
  12.         if(q && key==q->key)  
  13.             return &(q->value);  
  14.     }  
  15.     return NULL;  
  16. }  

跳錶的銷燬

上面分別介紹了跳錶的建立、節點插入、節點刪除,其中涉及了內存的動態分配,在使用完跳錶後別忘了釋放所申請的內存,否則會內存泄露的。

很少說了,代碼以下:

 

[cpp]  view plain copy
 
  1. // 釋放跳躍表  
  2. void sl_free(skip_list *sl)  
  3. {  
  4.     if(!sl)  
  5.         return;  
  6.       
  7.     Node *q=sl->head;  
  8.     Node *next;  
  9.     while(q)  
  10.     {  
  11.         next=q->next[0];  
  12.         free(q);  
  13.         q=next;  
  14.     }  
  15.     free(sl);  
  16. }  

 

skiplist複雜度分析

skiplist分析以下圖(摘自 這裏)

 

       



 

 

參考:

https://www.cs.auckland.ac.nz/software/AlgAnim/niemann/s_skl.htm

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

相關文章
相關標籤/搜索