跳躍表(skiplist)是一種有序資料結構,它通過在每個節點中維持多個指向其他節點的指標,從而達到快速訪問節點的目的。
跳躍表支援平均O(logN)、最壞O(N)複雜度的節點查詢,還可以通過順序性操作來批量處理節點。在大部分情況下,跳躍表的效率可以和平衡樹相媲美,並且因為跳躍表的實現比平衡樹要來得更為簡單,所以有不少程式都使用跳躍表來代替平衡樹。
Redis使用跳躍表作為有序集合鍵的底層實現之一,如果一個有序集合包含的元素數量比較多,又或者有序集合中元素的成員(member)是比較長的字串時,Redis就會使用跳躍表來作為有序集合鍵的底層實現。
以下是個典型的跳躍表例子
從圖中可以看到, 跳躍表主要由以下部分構成:
- 表頭(head):負責維護跳躍表的節點指標。
- 跳躍表節點:儲存著元素值,以及多個層。
- 層:儲存著指向其他元素的指標。高層的指標越過的元素數量大於等於低層的指標,為了提高查詢的效率,程式總是從高層先開始訪問,然後隨著元素值範圍的縮小,慢慢降低層次。
- 表尾:全部由
NULL
組成,表示跳躍表的末尾。
因為跳躍表的定義可以在任何一本演算法或資料結構的書中找到, 所以本章不介紹跳躍表的具體實現方式或者具體的演算法, 而只介紹跳躍表在 Redis 的應用、核心資料結構和 API 。
跳躍表的實現
為了滿足自身的功能需要, Redis 基於 William Pugh 論文中描述的跳躍表進行了以下修改:
- 允許重複的
score
值:多個不同的member
的score
值可以相同。 - 進行對比操作時,不僅要檢查
score
值,還要檢查member
:當score
值可以重複時,單靠score
值無法判斷一個元素的身份,所以需要連member
域都一併檢查才行。 - 每個節點都帶有一個高度為 1 層的後退指標,用於從表尾方向向表頭方向迭代:當執行 ZREVRANGE 或 ZREVRANGEBYSCORE 這類以逆序處理有序集的命令時,就會用到這個屬性。
這個修改版的跳躍表由 redis.h/zskiplist
結構定義:
typedef struct zskiplist {
// 頭節點,尾節點
struct zskiplistNode *header, *tail;
// 節點數量
unsigned long length;
// 目前表內節點的最大層數
int level;
} zskiplist;
複製程式碼
跳躍表的節點由 redis.h/zskiplistNode
定義:
typedef struct zskiplistNode {
// member 物件
robj *obj;
// 分值
double score;
// 後退指標
struct zskiplistNode *backward;
// 層
struct zskiplistLevel {
// 前進指標
struct zskiplistNode *forward;
// 這個層跨越的節點數量
unsigned int span;
} level[];
} zskiplistNode;
複製程式碼
以下是操作這兩個資料結構的 API ,API 的用途與相應的演算法複雜度:
函式 | 作用 | 複雜度 |
---|---|---|
zslCreateNode |
建立並返回一個新的跳躍表節點 | 最壞 O(1)O(1) |
zslFreeNode |
釋放給定的跳躍表節點 | 最壞 O(1)O(1) |
zslCreate |
建立並初始化一個新的跳躍表 | 最壞 O(1)O(1) |
zslFree |
釋放給定的跳躍表 | 最壞 O(N)O(N) |
zslInsert |
將一個包含給定 score 和 member 的新節點新增到跳躍表中 |
最壞 O(N)O(N) 平均 O(logN)O(logN) |
zslDeleteNode |
刪除給定的跳躍表節點 | 最壞 O(N)O(N) |
zslDelete |
刪除匹配給定 member 和 score 的元素 |
最壞 O(N)O(N) 平均 O(logN)O(logN) |
zslFirstInRange |
找到跳躍表中第一個符合給定範圍的元素 | 最壞 O(N)O(N) 平均 O(logN)O(logN) |
zslLastInRange |
找到跳躍表中最後一個符合給定範圍的元素 | 最壞 O(N)O(N) 平均 O(logN)O(logN) |
zslDeleteRangeByScore |
刪除 score 值在給定範圍內的所有節點 |
最壞 O(N2)O(N2) |
zslDeleteRangeByRank |
刪除給定排序範圍內的所有節點 | 最壞 O(N2)O(N2) |
zslGetRank |
返回目標元素在有序集中的排位 | 最壞 O(N)O(N) 平均 O(logN)O(logN) |
zslGetElementByRank |
根據給定排位,返回該排位上的元素節點 | 最壞 O(N)O(N) 平均 O(logN)O(logN) |
跳躍表的應用
和字典、連結串列或者字串這幾種在 Redis 中大量使用的資料結構不同, 跳躍表在 Redis 的唯一作用, 就是實現有序集資料型別。
跳躍表將指向有序集的 score
值和 member
域的指標作為元素, 並以 score
值為索引, 對有序集元素進行排序。
舉個例子, 以下程式碼建立了一個帶有 3 個元素的有序集:
redis> ZADD s 6 x 10 y 15 z
(integer) 3
redis> ZRANGE s 0 -1 WITHSCORES
1) "x"
2) "6"
3) "y"
4) "10"
5) "z"
6) "15"
複製程式碼
在底層實現中, Redis 為 x
、 y
和 z
三個 member
分別建立了三個字串, 值分別為 double
型別的 6
、 10
和 15
, 然後用跳躍表將這些指標有序地儲存起來, 形成這樣一個跳躍表:
為了方便展示, 在圖片中我們直接將 member
和 score
值包含在表節點中, 但是在實際的定義中, 因為跳躍表要和另一個實現有序集的結構(字典)分享 member
和 score
值, 所以跳躍表只儲存指向 member
和 score
的指標。 更詳細的資訊,請參考《有序集》章節。
小結
❑跳躍表是有序集合的底層實現之一。
❑Redis的跳躍表實現由zskiplist和zskiplistNode兩個結構組成,其中zskiplist用於儲存跳躍表資訊(比如表頭節點、表尾節點、長度),而zskiplistNode則用於表示跳躍表節點。
❑每個跳躍表節點的層高都是1至32之間的隨機數。
❑在同一個跳躍表中,多個節點可以包含相同的分值,但每個節點的成員物件必須是唯一的。
❑跳躍表中的節點按照分值大小進行排序,當分值相同時,節點按照成員物件的大小進行排序。
參考地址
- redisbook.readthedocs.io/en/latest/i…
- 書籍:《Redis設計與實現》
如果大家喜歡我的文章,可以關注個人訂閱號。歡迎隨時留言、交流。如果想加入微信群的話一起討論的話,請加管理員簡棧文化-小助手(lastpass4u),他會拉你們進群。