鏈表簡介
鏈表提供了高效的節點重排能力,以及順序性的節點訪問方式,並且可以通過增刪節點來靈活地跳轉鏈表的長度。
作為一種常用數據結構,鏈表內置在很多高級的編程語言里面,因為Redis使用C語言並沒有內置這種數據結構,所以Redis構建了自己的鏈表實現。
鏈表在Redis中的應用非常多,比如列表鍵的底層實現之一就是鏈表,發布與訂閱、慢查詢、監視器等功能也用到了鏈表,Redis服務器本身還是用鏈表來保存多個客戶端的狀態信息,以及使用鏈表來構建客戶端輸出緩沖區(output buffer)。
鏈表和鏈表節點的實現
每個鏈表的節點都是一個adlist.h/ listNode結構
typedef struct listNode { struct listNode *prev;//前置節點 struct listNode *next;//后置節點 void *value;//節點的值 } listNode;
雖然使用多個listNode結構就可以組成鏈表,但使用adlist.h/ list來持有鏈表的話,操作起來會更方便。
typedef struct list { //表頭節點 listNode *head; //表尾節點 listNode *tail; //節點值復制函數 void *(*dup)(void *ptr); //節點值釋放制函數 void (*free)(void *ptr); //節點值對比函數 int (*match)(void *ptr, void *key); //鏈表所包含的節點數量 unsigned long len; } list;
Redis的鏈表特性總結
1、 雙端:鏈表節點帶有prev和next指針,獲取某個節點的前置節點和后置節點的復雜度都是O(1)。
2、 無環:表頭節點的prev指針和表位節點的next指針都指向null,對鏈表的訪問以null為終點。
3、 帶表頭指針和表尾指針:通過list結構的head指針和tail指針,程序獲取鏈表的表頭節點和表尾節點的復雜度為O(1)。
4、 帶鏈表長度計數器:程序使用list結構的len屬性來對list持有的鏈表節點進行計數,所以獲取節點數量的復雜度為O(1)。
5、 多態:鏈表節點使用void *指針來保存節點值,並且可以通過list結構的dup、free、match三個屬性為節點設置類型特定函數,所以鏈表可以用於保存各種不同類型的值。
鏈表和鏈表節點的API
跳躍表(skipList)簡介
跳躍表(skipList)是一種有序數據結構,他通過在每個節點中維持多個指向其他節點的指針,從而達到快速訪問節點的目的。
跳躍表支持評價O(logN)、最壞O(N)復雜度的節點查找,還可以通過順序性操作來批量處理節點。
在大部分情況下,跳躍表的效率可以和平衡樹相媲美,並且因為跳躍表的實現比平衡樹來得更簡單,所以有不少程序都是用跳躍表來代替平衡樹。
具體關於跳躍表的原理,可查看這個《漫畫算法:什么是跳躍表》http://blog.jobbole.com/111731/。
Redis使用跳躍表作為有序結合鍵的底層實現之一,如果一個有序集合包含的元素數量比較多,又或者有序集合中元素的成員時比較長的字符串時,redis就會使用跳躍表來作為有序集合鍵的底層實現。
Redis在兩個地方用到了跳躍表,一個是實現有序集合鍵,另一個是在集群節點中用作內部數據結構。
跳躍表的實現
Redis的跳躍表由redis.h\zskiplistNode和redis.h/zskiplist兩個結構定義(最新版已經移動到server.h),其中zskiplistNode結構用於表示跳躍表節點,而zskiplist結構則用於保存跳躍表節點的相關信息,比如節點數量,以及指向表頭界定啊和表尾節點的指針等等。
上圖最左邊的就是zskiplist結構,該結構包含以下屬性:
1 header:指向跳躍表的表頭表頭節點。
2 tail:指向跳躍表的表尾節點。
3 level:記錄目前跳躍表內,層數最大的那個節點的層數(表頭節點的層數不計算在內)。
4 length:記錄跳躍表的長度,也即是,跳躍表目前包含節點的數量(表頭節點不計算在內)。
位於zskiplist結構右方的四個zskiplistNode結構,該結構包含一下屬性:
1 層(level):節點中用L1、L2、L3等字樣標記節點的各個層,L1代表第一層,L2代表第二層,以此類推。每個層都帶有兩個屬性:前進指針和跨度。前進指針用於訪問位於表尾方向的其他節點,而跨度則記錄了前進指針所指向節點和當前節點的舉例。在上圖中,連線上帶有數字的箭頭就代表前進指針,而那個數字就是跨度。當程序從表頭向表尾進行遍歷時,訪問會沿着層的前進指針進行。
2 后退指針:節點中用BW(backward)字樣標記節點的后退指針,他指向位於當前節點的前一個節點。后退指針在程序從表尾向表頭遍歷時使用。
3 分值(score):各個節點中的1.0、2.0和3.0是節點所保存的分值。在跳躍表中,節點按各自所保存的分值從小到大排量。
4 成員對象(obj):各個節點中o1、o2和o3是節點所保存的成員對象。
注意:表頭節點和其他節點的構造是一樣的;表頭節點也有后退指針、分值和成員對象,不過表頭節點的這些屬性不會被用到,所以圖中省略了。
跳躍表節點zskiplistNode
跳躍表節點由server.h\zskiplistNode結構定義
/* ZSETs use a specialized version of Skiplists */ typedef struct zskiplistNode { //成員對象 robj *obj; //分值 double score; //后退指針 struct zskiplistNode *backward; //層 struct zskiplistLevel { struct zskiplistNode *forward;//前進指針 unsigned int span;//跨度 } level[]; } zskiplistNode;
1、 層
跳躍表節點的level數組可以包含多個元素,每個元素都包含一個指向其他節點的指針,程序可以通這些層來加快訪問其他節點的速度,一般來說,層數越多,訪問其他節點的速度就越快。
每次創建一個新的跳躍表節點的時候,程序都根據冪次定律(power law,越大的數出現的概率越小)隨機生成一個介於1和32之間的值作為level數組的大小,這個大小就是層的“高度”。下圖就是帶有不同層高的節點。
2、 前進指針
每個層都有一個指向表尾方向的前進指針,用於從表頭向表尾方向訪問節點。下圖用虛線表示出了程序從表頭向表尾方向,遍歷跳躍表中所有節點的路徑:
a 迭代程序首先訪問跳躍表的第一個節點(表頭),然后從第四層的前進指針移動到表中的第二個節點。
b 在第二個節點時,程序沿着第二層的前進指針移動到表中第三個節點。
c 在第三個節點時,程序同樣沿着第二層的前進指針移動到表中的第四個節點。
d 當程序再次沿着第四個節點的前進指針移動式,他碰到一個null,程序知道這時已經到達了跳躍表的表尾,於是結束這次遍歷。
3、跨度
層的跨度用於記錄兩個節點之間的距離:
a 兩個節點之間的跨度越大,他們相距得就越遠。
b 指向null的所有前進指針的跨度都為0,因為他們沒有連向任何節點。
初看上去,很容易以為跨度和遍歷操作有關,但實際上並不是這樣,遍歷操作只使用前進指針就可以完成了,寬度實際上是用來計算排位(rank)的:在查找某個節點的過程中,將沿途訪問過的所有層的跨度累計起來,得到的結果就是目標節點在跳躍表中的排位。
舉個例子,下圖用虛線標記了在跳躍表中查找分值為3.0、成員對象為o3的節點時,沿途經歷的層:查找的過程只經過了一個層,並且層的跨度為3,所以目標節點在跳躍表中的排位為3。
再舉個例子,下圖用虛線標記了在跳躍表中查找分值為2.0、成員對象為o2的節點時,沿途經歷的層:在查找節點的過程中,程序經過了兩個跨度為1的節點,因此可以計算出,目標節點在跳躍表中的排位為2。
4、后退指針
節點的后退指針用於從表尾向表頭方向訪問節點:跟可以一次跳過多個節點的前進指針不同,因為每個節點只有一個后退指針,所以每次只能后退至前一個節點。
下圖用虛線表示了如果從表尾向表頭遍歷跳躍表中的所有節點。
5、分值和成員
節點的分值(score屬性)是一個double類型的浮點數,跳躍表中的所有節點都按照分值從小到大來排序。
節點的成員對象是一個指針,他指向一個字符串對象,而字符串對象則保存着一個SDS值。
在同一個跳躍表中,各個節點保存的成員對象必須是唯一的,但是多個節點保存的分值卻可以是相同的:分值相同的節點按照成員對象在字典中的大小來進行排序,成員對象較小的節點會排在前面(靠近表頭方向),而成員對象較大的節點則會排在后面(靠近表尾的方向)。
舉個例子,在下圖所示的跳躍表中,三個跳躍表節點都保存了相同的分值10086.0,但保存成員對象o1的節點卻排在保存成員對象o2和o3的節點之前,由順序可知,三個對象在字典中的排序哦o1<=o2<=o3。
跳躍表zskiplist
緊靠多個跳躍表節點就可以組成一個跳躍表,如下圖。
但通過使用一個zskiplist結構來持有這些節點,程序可以更方便地對整個跳躍表進行處理,比如快速訪問跳躍表的表頭節點和表尾節點,或者快速地獲取跳躍表節點的數量等信息。
typedef struct zskiplist { //表頭節點和表尾節點 struct zskiplistNode *header, *tail; //表中節點的的數量 unsigned long length; //表中層數最大的節點層數 int level; } zskiplist;
header和tail指針分別指向跳躍表的表頭和表尾節點,通過這兩個指針,程序定位表頭及誒點和表尾節點的復雜度為O(1)。
通過使用length屬性來記錄節點的數量,程序可以在O(1)復雜度內返回跳躍表的長度。
level屬性則用於在O(1)復雜度內獲取跳躍表中層高最大的那個節點的層數量,注意表頭節點的層高並不計算在內。
跳躍表API