跳躍表(skiplist)是一種有序資料結構,它通過在每個節點中維持多個指向其他節點的指標,從而達到快速訪問節點的目的。
跳躍表支援平均o(logn)、最壞o(n)複雜度的節點查詢,還可以通過順序性操作來批量處理節點。
在大部分情況下,跳躍表的效率可以和平衡樹相媲美,並且因為跳躍表的實現比平衡樹要來得更為簡單,所以有不少程式都使用跳躍表來代替平衡樹。
redis使用跳躍表作為有序集合鍵的底層實現之一,如果乙個有序集合包含的元素數量比較多,又或者有序集合中元素的成員(member)是比較長的字串時,redis就會使用跳躍表來作為有序集合鍵的底層實現。
和鍊錶、字典等資料結構被廣泛地應用在redis內部不同,redis只在兩個地方用到了跳躍表,乙個是實現有序集合鍵,另乙個是在集群節點中用作內部資料結構,除此之外,跳躍表在redis裡面沒有其他用途。
跳躍表的實現
redis的跳躍表由redis.h/zskiplistnode和redis.h/zskiplist兩個結構定義,其中zskiplistnode結構用於表示跳躍表節點,而zskiplist結構則用於儲存跳躍表節點的相關資訊,比如節點的數量,以及指向表頭節點和表尾節點的指標等等。
位於zskiplist結構右方的是四個zskiplistnode結構,該結構包含以下屬性:
注意表頭節點和其他節點的構造是一樣的:表頭節點也有後退指標、分值和成員物件,不過表頭節點的這些屬性都不會被用到,所以圖中省略了這些部分,只顯示了表頭節點的各個層。
跳躍表節點
跳躍表節點的實現由redis.h/zskiplistnode結構定義:
/* zsets use a specialized version of skiplists */
typedef struct zskiplistnode level;
} zskiplistnode;
1、分值和成員
節點的分值(score屬性)是乙個double型別的浮點數,跳躍表中的所有節點都按分值從小到大來排序。
節點的成員物件(obj屬性)是乙個指標,它指向乙個字串物件,而字串物件則儲存著乙個sds值。
在同乙個跳躍表中,各個節點儲存的成員物件必須是唯一的,但是多個節點儲存的分值卻可以是相同的:分至相同的節點將按照成員物件在字典中的大小來進行排序,成員物件較小的節點會排在前面(靠近表頭的方向),而成員物件較大的節點則會排在後面(靠近表尾的方向)。
舉個例子,在下圖中所示的跳躍表中,三個跳躍表節點都儲存了相同的分值10086.0,但儲存成員物件o1的節點卻排在儲存成員物件o2和o3的節點的前面,而儲存成員物件o2的節點又排在儲存成員物件o3的節點之前,由此可見,o1、o2、o3三個成員物件在字典中的排序為o1<=o2<=o3。
2、後退指標
節點的後退指標(backward屬性)用於從表尾向表頭方向訪問節點:跟可以一次跳過多個節點的前進指標不同,因為每個節點只有乙個後退指標,所以每次只能後退至前乙個節點。
下圖用虛線展示了如何從表尾向表頭遍歷跳躍表中的所有節點:程式首先通過跳躍表的tail指標訪問表尾節點,然後通過後退指標訪問倒數第二個節點,之後再沿著後退指標訪問倒數第三個節點,再之後遇到指向null的後退指標,於是訪問結束。
3、層跳躍表節點的level陣列可以包含多個元素,每個元素都包含乙個指向其他節點的指標,程式可以通過這些層來加快訪問其他節點的速度,一般來說,層的數量越多,訪問其他節點的速度就越快。
每次建立乙個新跳躍表節點的時候,程式根據冪次定律(power law,越大的數出現的概率越小)隨機生成乙個介於1和32之間的值作為level陣列的大小,這個大小就是層的「高度」。
下圖分別展示了三個高度為1層、3層和5層的節點,因為c語言的陣列索引總是從0開始的,所以節點的第一層是level[0],而第二層是level[1],依次類推。
4、前進指標
每個層都有乙個指向表尾方向的前進指標(level[i].forward屬性),用於從表頭向表尾方向訪問節點。下圖用虛線表示出了程式從表頭向表尾方向,遍歷跳躍表中所有節點的路徑:
1) 迭代程式首先訪問跳躍表的第乙個節點(表頭),然後從第四層的前進指標移動到表中的第二個節點。
2) 在第二個節點時,程式沿著第二層的前進指標移動到表中的第三個節點。
3) 在第三個節點時,程式同樣沿著第二層的前進指標移動到表中的第四個節點。
4) 當程式再次沿著第四個節點的前進指標移動時,它碰到乙個null,程式知道這時已經到達了跳躍表的表尾,於是結束這次遍歷。
5、跨度
層的跨度(level[i].span屬性)用於記錄兩個節點之間的距離:
兩個節點之間的跨度越大,它們相距得就越遠。
指向null的所有前進指標的跨度都為0,因為它們沒有連向任何節點。
初看上去,很容易以為跨度和遍歷操作有關,但實際上並不是這樣的,遍歷操作只使用前進指標就可以完成了,跨度實際上是用來計算排位(rank)的:在查詢某個節點的過程中,將沿途訪問過的所有層的跨度累計起來,得到的結果就是目標節點在跳躍表中的排位。
舉個例子,下圖用虛線標記了在跳躍表中查詢分值為3.0、成員物件為o3的節點時,沿途經歷的層:查詢的過程只經過了乙個層,並且層的跨度為3,所以目標節點在跳躍表中的排位為3。
再舉個例子,下圖用虛線標記了在跳躍表中查詢分值為2.0、成員物件為o2的節點時,沿途經歷的層:在查詢節點的過程中,程式經過了兩個跨度為1的節點,因此可以計算出,目標節點在跳躍表中的排位為2。
跳躍表僅靠多個跳躍表節點就可以組成乙個跳躍表,如下圖所示:
但通過使用乙個zskiplist結構來持有這些節點,程式可以更方便地對整個跳躍表進行處理,比如快速訪問跳躍表的表頭節點和表尾節點,或者快速地獲取跳躍表節點的數量(也即是跳躍表的長度)等資訊,如下圖所示:
zskiplist結構的定義如下:
typedef struct zskiplist zskiplist;
這樣獲取表頭、表尾節點,表長,以及表中最高層數的複雜度均為o(1)。 Redis設計與實現 資料結構與物件(二)
定義 typedef struct list list 注意,可以返回鍊錶的長度,本質上是乙個雙端鍊錶 相關的api 函式作用 演算法複雜度 listcreate建立新鍊錶 o 1 o 1 listrelease釋放鍊錶,以及該鍊錶所包含的節點 o n o n listdup建立給定鍊錶的副本 o ...
Redis設計與實現 02 資料結構與物件
redis設計與實現 黃建巨集版的讀書筆記 struct sdshr c字串 sds獲取字串長度的時間複雜度為o n 獲取字串長度的時間複雜度為o 1 api 是不安全的,可能會造成緩衝區溢位 api 是安全的,不會造成緩衝區溢位 修改字串長度n次必然需要執行n次記憶體重分配 修改字串長度n次最多需...
Redis設計與實現 筆記(1) 基礎資料結構篇
1 redis自己構建了乙個名為sds dynamic string 的字串資料結構。struct sdshdr 遵循了c字串以空字元 佔1位元組 結尾的慣例,且該空字元不計入len屬性 2 sds的空間分配策略 1 listnodetypedef struct listnodelistnode 2...