跳躍表
跳躍表 是一種有序的數據結構,通過在每個節點中維持多個指向其他節點的指針,從而達到快速訪問節點的目的。它是基於有序鏈表,在鏈表的基礎上,每個節點不止包含一個指針,還可能包含多個指向後繼節點的指針,從而加快操作。
示例:
結合圖,比如查找key爲3
是否存在:
- 首先訪問的是key
1
,比3
小,然後從最高層的後繼節點4
進行比較,3
比4
小,所以3
肯定在1
和4
之間; - 因此移向下一層,後繼節點爲
3
剛好滿足。
時間複雜度
跳躍表支持平均O(logN)
,最壞O(N)
時間複雜度的節點查找。
在大部分情況下,跳躍表的效率可以和平衡樹相媲美,並且跳躍表的實現比平衡樹簡單。
特點
來之《Redis設計與實現》
- 跳躍表是有序集合的底層實現之一。Redis只在實現有序集合鍵和集羣節點的內部數據結構中用到了跳躍表。
- Redis的跳躍表實現由zskiplist和zskiplistNode兩個結構組成,其中zskiplist用於保存跳躍表信息(比如表頭節點、表尾節點、長度),而zskiplistNode則用於表示跳躍表節點。
- 每個跳躍表節點的層高都是1至32之間的隨機數(冪次定律,越大的數出現的 概率越小)。
- 在同一個跳躍表中,多個節點可以包含相同的分值,但每個節點的成員對象必須是唯一的。
- 跳躍表中的節點按照分值大小進行排序,當分值相同時,節點按照成員對象的大小進行排序。
代碼結構
// server.h
/* ZSETs use a specialized version of Skiplists */
// 跳躍表節點 結構
typedef struct zskiplistNode {
// 節點的成員
// 在redis3.0 中使用的是 robj
// 在redis4.0 中使用 sds
sds ele;
double score; // 分值
struct zskiplistNode *backward; // 後退指針
// 層
struct zskiplistLevel {
struct zskiplistNode *forward; // 層的前進指針
unsigned int span; // 層的跨度
} level[];
} zskiplistNode;
成員ele
: 在同一個跳躍表中,各個節點保存的成員對象必須是唯一的。
分值score
: 跳躍表中,所有節點都按分值從小到大排序。不同節點中的分值可以相同,分值相同的節點將按照成員對象的大小來排序,成員對象較小的節點在前面。
後退指針*backward
: 用於從表尾向表頭方向訪問節點,跟可以一次跳過多個節點的前進指針不同,因爲每個節點只有一個後退指針,所以每次只能後退至前一個節點。
層level[]
:跳躍表節點的level數組可以包含多個元素,每個元素都包含一個指向其他節點的指針,程序可以通過這些層來加快訪問其他節點的速度,一般來說,層的數量越多,訪問其他節點的速度就越快。每次創建一個新跳躍表節點的時候,程序都根據冪次定律(powerlaw,越大的數出現的概率越小)隨機生成一個介於1和32之間的值作爲level數組的大小,這個大小就是層的“高度”。
前進指針*forward
: 每個層都有一個指向表尾方向的前進指針,用於從表頭向表尾方向訪問節點。
跨度span
: 層的跨度用於記錄連個節點之間的距離:
- 兩個節點之間的跨度越大,它們相距得就越遠。
- 指向NULL的所有前進指針的跨度都爲0,因爲它們沒有連向任何節點。
// 跳躍表 結構
typedef struct zskiplist {
struct zskiplistNode *header, *tail; // 記錄 表頭節點 和 表尾節點 的指針
unsigned long length; // 表中節點的數量
int level; // 表中層數最大的節點層數
} zskiplist;
部分代碼解析
-
zskiplist *zslCreate(void)
創建一個空的zskiplist
:// t_zet.c /* Create a new skiplist. */ zskiplist *zslCreate(void) { int j; zskiplist *zsl; zsl = zmalloc(sizeof(*zsl)); zsl->level = 1; zsl->length = 0; // 創建一個空節點給表頭節點 zsl->header = zslCreateNode(ZSKIPLIST_MAXLEVEL,0,NULL); for (j = 0; j < ZSKIPLIST_MAXLEVEL; j++) { // 爲頭節點的各個層級賦值 // 前進指針forward 置爲 NULL // 跨度span 設爲 0 zsl->header->level[j].forward = NULL; zsl->header->level[j].span = 0; } // 因爲該跳躍表是空的,所以 表頭節點的後退指針 置爲 NULL zsl->header->backward = NULL; // 表尾節點 置爲 NULL zsl->tail = NULL; return zsl; }
-
zskiplistNode *zslCreateNode(int level, double score, sds ele)
創建一個跳躍表節點,創建level
層數,分值score
,節點成員ele
:// t_zet.c /* Create a skiplist node with the specified number of levels. * The SDS string 'ele' is referenced by the node after the call. */ zskiplistNode *zslCreateNode(int level, double score, sds ele) { // 爲層數level數組分配內存 zskiplistNode *zn = zmalloc(sizeof(*zn)+level*sizeof(struct zskiplistLevel)); // 分值賦值 zn->score = score; // 成員賦值 zn->ele = ele; return zn; }
-
zskiplistNode *zslInsert(zskiplist *zsl, double score, sds ele)
往跳躍表*zsl
中插入成員ele
,分值是score
:// t_zet.c /* Insert a new node in the skiplist. Assumes the element does not already * exist (up to the caller to enforce that). The skiplist takes ownership * of the passed SDS string 'ele'. */ zskiplistNode *zslInsert(zskiplist *zsl, double score, sds ele) { zskiplistNode *update[ZSKIPLIST_MAXLEVEL], *x; unsigned int rank[ZSKIPLIST_MAXLEVEL]; int i, level; serverAssert(!isnan(score)); x = zsl->header; for (i = zsl->level-1; i >= 0; i--) { /* store rank that is crossed to reach the insert position */ rank[i] = i == (zsl->level-1) ? 0 : rank[i+1]; while (x->level[i].forward && (x->level[i].forward->score < score || (x->level[i].forward->score == score && sdscmp(x->level[i].forward->ele,ele) < 0))) { rank[i] += x->level[i].span; x = x->level[i].forward; } update[i] = x; } /* we assume the element is not already inside, since we allow duplicated * scores, reinserting the same element should never happen since the * caller of zslInsert() should test in the hash table if the element is * already inside or not. */ level = zslRandomLevel(); if (level > zsl->level) { for (i = zsl->level; i < level; i++) { rank[i] = 0; update[i] = zsl->header; update[i]->level[i].span = zsl->length; } zsl->level = level; } x = zslCreateNode(level,score,ele); for (i = 0; i < level; i++) { x->level[i].forward = update[i]->level[i].forward; update[i]->level[i].forward = x; /* update span covered by update[i] as x is inserted here */ x->level[i].span = update[i]->level[i].span - (rank[0] - rank[i]); update[i]->level[i].span = (rank[0] - rank[i]) + 1; } /* increment span for untouched levels */ for (i = level; i < zsl->level; i++) { update[i]->level[i].span++; } x->backward = (update[0] == zsl->header) ? NULL : update[0]; if (x->level[0].forward) x->level[0].forward->backward = x; else zsl->tail = x; zsl->length++; return x; }
-
插入第一個節點(假設節點
score
爲1,ele
爲a):-
初始化狀態
-
獲得插入位置
x = zsl->header; // 當跳躍表本身爲空時,zsl->level 爲 1 // 所以這個循環只執行1次 for (i = zsl->level-1; i >= 0; i--) { /* store rank that is crossed to reach the insert position */ // 所以rank[0] = 0 rank[i] = i == (zsl->level-1) ? 0 : rank[i+1]; // 因爲 x 指向 zsl 的表頭節點, // 表頭節點 所有的level 的前進指針都爲NULL // 所以插入第一個節點上時,不進入這個循環 while (x->level[i].forward && (x->level[i].forward->score < score || (x->level[i].forward->score == score && sdscmp(x->level[i].forward->ele,ele) < 0))) { rank[i] += x->level[i].span; x = x->level[i].forward; } // 將update[0]指向表頭節點 update[i] = x; }
-
插入節點
// 該函數會生成一個隨機(1-32之間)的level(假設爲4) level = zslRandomLevel(); // 如果生成的level大於zsl->level(當前爲1) if (level > zsl->level) { // i 從 1 開始,循壞到 i = 3 for (i = zsl->level; i < level; i++) { // rank[1],rank[2],rank[3] 設爲 0 rank[i] = 0; // update[1],update[2],update[3] 都指向表頭節點 update[i] = zsl->header; // update[1]->level[1].span,update[2]->level[2].span,update[3]->level[3].span 都設爲 0 // 實際上就是將 header的level[1,2,3]的span都設爲0 update[i]->level[i].span = zsl->length; } // 更新zls->level的值 zsl->level = level; }
// 創建表節點 x = zslCreateNode(level,score,ele); // level 爲4,循環 i = [0, 1, 2, 3] for (i = 0; i < level; i++) { // x 是指向新建節點的 // update[0-3]是指向頭節點 // 而頭節點的level[0-3]的前進節點都是NULL // 所以新建節點x->level[0-3]的前進節點都指向NULL x->level[i].forward = update[i]->level[i].forward; // 將頭節點的level[0-3]的前進節點都指向新建節點x update[i]->level[i].forward = x; /* update span covered by update[i] as x is inserted here */ // 更新 新建節點x->level[0-3]的層跨度 // 而 表頭節點->level[0-3].span 的層跨度都爲0 // rank[0-3] 爲 0 // 所以 新建節點x->level[0-3]的層跨度 都設爲0 x->level[i].span = update[i]->level[i].span - (rank[0] - rank[i]); // 更新表頭節點的level[0-3]層跨度 = 1 update[i]->level[i].span = (rank[0] - rank[i]) + 1; }
-
更新其他節點的level信息
/* increment span for untouched levels */ // level 爲4 // zsl->level 也爲 4 // 所以不進入該循環 for (i = level; i < zsl->level; i++) { update[i]->level[i].span++; } // 新建節點的信息指向NULL x->backward = (update[0] == zsl->header) ? NULL : update[0]; // x->level[0].forward 指向 NULL if (x->level[0].forward) x->level[0].forward->backward = x; else // 表中只有1個結點,將表尾指針指向新建節點x zsl->tail = x; // zsl 的長度 + 1 zsl->length++; return x;
-
-
向有數據的跳躍表插入
-
假設跳躍表中有如下節點,假設插入節點
score
爲2,ele
爲b: -
獲得插入位置
x = zsl->header; // i 初始值爲 4,所以循環會執行 i[4, 3, 2, 1, 0] 5次 for (i = zsl->level-1; i >= 0; i--) { /* store rank that is crossed to reach the insert position */ rank[i] = i == (zsl->level-1) ? 0 : rank[i+1]; // 循環尋找出要定位的位置 // 直至找到末尾,或者參數分值小於節點分值 // 如果找到相同分值,則比較成員的大小 // 隨着 i 的變小,x將會指向插入位置前方的節點 while (x->level[i].forward && (x->level[i].forward->score < score || (x->level[i].forward->score == score && sdscmp(x->level[i].forward->ele,ele) < 0))) { // 累積記下節點移動的跨度 rank[i] += x->level[i].span; // 更新x的指向,在while循環中x向前移動 x = x->level[i].forward; } update[i] = x; }
-
爲新增的節點隨機生成level(假設生成的是6)
// 獲取一個隨機的level數,假設生成的是6 level = zslRandomLevel(); if (level > zsl->level) { // 如果隨機生成的level 比原來跳躍表的level大,則需要對錶頭節點的level擴展 for (i = zsl->level; i < level; i++) { rank[i] = 0; update[i] = zsl->header; update[i]->level[i].span = zsl->length; } zsl->level = level; }
-
創建並在跳躍表中插入新節點
// 創建節點,level=6, sorce=2, ele="b" x = zslCreateNode(level,score,ele); for (i = 0; i < level; i++) { // 因爲 *update數組 中保存的都是在 插入位置 之前的節點 // 所以在這裏將x中level的前進指針 指向 *update數組中節點的前進指針指向的節點 x->level[i].forward = update[i]->level[i].forward; // 然後 *update數組中節點的前進指針 指向 x update[i]->level[i].forward = x; /* update span covered by update[i] as x is inserted here */ // 更新 x 中各個level的跨度 x->level[i].span = update[i]->level[i].span - (rank[0] - rank[i]); // 更新 *update數組中節點的level跨度 update[i]->level[i].span = (rank[0] - rank[i]) + 1; }
-
更新其他節點的level信息
/* increment span for untouched levels */ // 更新其他節點的跨度,本例中level和zsl->level已經相等,所以不進入循環 for (i = level; i < zsl->level; i++) { update[i]->level[i].span++; } // 更新插入節點的後退指針 x->backward = (update[0] == zsl->header) ? NULL : update[0]; if (x->level[0].forward) x->level[0].forward->backward = x; else zsl->tail = x; zsl->length++; return x;
-
-
-
int zslDelete(zskiplist *zsl, double score, sds ele, zskiplistNode **node)
通過指定score
和else
刪除節點,如果在跳躍表中找到節點,函數將會返回1,並且將刪除的節點寫入**node
中;如果沒有找到,將會返回0:/* Delete an element with matching score/element from the skiplist. * The function returns 1 if the node was found and deleted, otherwise * 0 is returned. * * If 'node' is NULL the deleted node is freed by zslFreeNode(), otherwise * it is not freed (but just unlinked) and *node is set to the node pointer, * so that it is possible for the caller to reuse the node (including the * referenced SDS string at node->ele). */ int zslDelete(zskiplist *zsl, double score, sds ele, zskiplistNode **node) { zskiplistNode *update[ZSKIPLIST_MAXLEVEL], *x; int i; x = zsl->header; // 找出 要刪除節點 的前置節點,與zslInsert()方法中類似 // 而update數組中保存了 要刪除節點 在刪除之前各個 level 緊鄰的前置節點 for (i = zsl->level-1; i >= 0; i--) { while (x->level[i].forward && (x->level[i].forward->score < score || (x->level[i].forward->score == score && sdscmp(x->level[i].forward->ele,ele) < 0))) { x = x->level[i].forward; } update[i] = x; } /* We may have multiple elements with the same score, what we need * is to find the element with both the right score and object. */ x = x->level[0].forward; // 判斷分值和成員是否相同 if (x && score == x->score && sdscmp(x->ele,ele) == 0) { zslDeleteNode(zsl, x, update); if (!node) // 如果參數node 是 NULL的,則直接釋放掉要刪除的節點 zslFreeNode(x); else // 否則將node 指向 x *node = x; return 1; } return 0; /* not found */ } /* Internal function used by zslDelete, zslDeleteByScore and zslDeleteByRank */ // 刪除節點的內部函數 // *zsl 是指定的跳躍表 // *x 是要刪除的節點 // **update 是指向一個數組的指針,數組中保存了 要刪除節點 在刪除之前各個 level 緊鄰的前置節點 void zslDeleteNode(zskiplist *zsl, zskiplistNode *x, zskiplistNode **update) { int i; // 刪除節點x,並且更新表中x前置節點level的跨度和指向 for (i = 0; i < zsl->level; i++) { if (update[i]->level[i].forward == x) { // 如果是本來指向x的,則指向x對應level的前置節點 // 並且重新計算跨度 update[i]->level[i].span += x->level[i].span - 1; update[i]->level[i].forward = x->level[i].forward; } else { // 如果本來不是指向x的,則是指向x之後的節點,所以只需要跨度減1 update[i]->level[i].span -= 1; } } // 修改 要刪除節點x 的後一個節點的後退指針 if (x->level[0].forward) { x->level[0].forward->backward = x->backward; } else { // 如果x是尾節點,則修改尾結點的指向 zsl->tail = x->backward; } // 修改表中最大level值 while(zsl->level > 1 && zsl->header->level[zsl->level-1].forward == NULL) zsl->level--; // 節點x已經不再關聯,長度減1 zsl->length--; }
API
參考之《Redis設計與實現》
函數 | 作用 |
---|---|
zskiplist *zslCreate(void) | 創建一個新的跳躍表 |
void zslFree(zskiplist *zsl) | 釋放給定跳躍表,以及表中包含的所有節點 |
zskiplistNode *zslInsert(zskiplist *zsl, double score, sds ele) | 將包含給定成員和分值的新節點添加到跳躍表中 |
int zslDelete(zskiplist *zsl, double score, sds ele, zskiplistNode **node) | 刪除跳躍表中包含給定成員和分值的節點 |
unsigned long zslGetRank(zskiplist *zsl, double score, sds o) | 返回包含給定成員和分值的節點在跳躍表中的排位 |
zskiplistNode* zslGetElementByRank(zskiplist *zsl, unsigned long rank) | 返回跳躍表在給定排位上的節點 |
int zslIsInRange(zskiplist *zsl, zrangespec *range) | 給定一個分值範圍range ,如果跳躍表中有至少一個節點的分值在這個分值範圍內,返回1,否則返回0。zrangespec 是包含min ,max 的結構體。 |
zskiplistNode *zskiplistNode *zslFirstInRange(zskiplist *zsl, zrangespec *range) | 給定一個分值範圍range ,返回跳躍表中第一個符合這個範圍的節點 |
zskiplistNode *zslLastInRange(zskiplist *zsl, zlexrangespec *range) | 給定一個分值範圍range ,返回跳躍表中最後一個符合這個範圍的節點 |
unsigned long zslDeleteRangeByScore(zskiplist *zsl, zrangespec *range, dict *dict) | 給定一個分值範圍,刪除跳躍表中所有這個範圍內的節點(同時也會刪除dict 中對應的key) |
unsigned long zslDeleteRangeByRank(zskiplist *zsl, unsigned int start, unsigned int end, dict *dict) | 給定一個排位範圍,刪除跳躍表中所有這個範圍內的節點(同時也會刪除dict 中對應的key) |