skiplist介紹
跳錶(skip List)是一種隨機化的數據結構,基於並聯的鏈表,實現簡單,插入、刪除、查找的複雜度均爲O(logN)。跳錶的具體定義,請參考參考維基百科 點我 , 中文版 。跳錶是由 William Pugh 發明的,這位確實是個大牛,搞出一些很不錯的東西。簡單說來跳錶也是
鏈表的一種,只不過它在鏈表的基礎上增加了跳躍功能,正是這個跳躍的功能,使得在查找元素時,跳錶能夠提供O(log n)的時間複雜
度。紅黑樹等這樣的平衡數據結構查找的時間複雜度也是O(log n),但是要實現像紅黑樹這樣的數據結構並非易事,但是隻要你熟悉鏈表
的基本操作,再加之對跳錶原理的理解,實現一個跳錶數據結構就是一個很自然的事情了。
此外,跳錶在當前熱門的開源項目中也有很多應用,比如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; inext[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->keykey == key)//key已經存在的情況下
{
q->value = val;
return true;
}
/******************step2*******************/
//產生一個隨機層數level
int level = randomLevel();
//如果新生成的層數比跳錶的層數大
if(level>sl->level)
{
//在update數組中將新添加的層指向header
for(i=sl->level; ihead;
}
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->keykey) 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分析如下圖(摘自 這裏 )
完整代碼及其測試: https://github.com/ustcdane/skiplist/ , 接下來可以嘗試着分析Redis 源代碼中skiplist相關的數據結構了。
參考:
https://www.cs.auckland.ac.nz/software/AlgAnim/niemann/s_skl.htm
http://www.cnblogs.com/xuqiang/archive/2011/05/22/2053516.html