跳表(skiplist)是一個特俗的鏈表,相比一般的鏈表,有更高的查找效率,其效率可比擬于二叉查找樹。
一張關(guān)于跳表和跳表搜索過程如下圖:
http://wiki.jikexueyuan.com/project/redis/images/redis11.png" alt="" />
在圖中,需要尋找 68,在給出的查找過程中,利用跳表數(shù)據(jù)結(jié)構(gòu)優(yōu)勢,只比較了 3 次,橫箭頭不比較,豎箭頭比較。由此可見,跳表預(yù)先間隔地保存了有序鏈表中的節(jié)點(diǎn),從而在查找過程中能達(dá)到類似于二分搜索的效果,而二分搜索思想就是通過比較中點(diǎn)數(shù)據(jù)放棄另一半的查找,從而節(jié)省一半的查找時間。缺點(diǎn)即浪費(fèi)了空間,自古空間和時間兩難全。
插播一段:跳表在 1990 年由 William Pugh 提出,而紅黑樹早在 1972 年由魯?shù)婪颉へ悹柊l(fā)明了。紅黑樹在空間和時間效率上略勝跳表一籌,但跳表實現(xiàn)相對簡單得到程序猿們的青睞。Redis 和 Leveldb 中都有采用跳表。 這篇文章,借著 Redis 的源碼了解跳表的實現(xiàn)。
http://wiki.jikexueyuan.com/project/redis/images/redis12.png" alt="" />
從上圖中,總結(jié)跳表的性質(zhì):
Redis 中跳表數(shù)據(jù)結(jié)構(gòu)定義:
// 跳表節(jié)點(diǎn)結(jié)構(gòu)體
/* ZSETs use a specialized version of Skiplists */
typedef struct zskiplistNode {
// 節(jié)點(diǎn)數(shù)據(jù)
robj *obj;
// 分?jǐn)?shù),游戲分?jǐn)?shù)?按游戲分?jǐn)?shù)排序
double score;
// 后驅(qū)指針
struct zskiplistNode *backward;
// 前驅(qū)指針數(shù)組TODO
struct zskiplistLevel {
struct zskiplistNode *forward;
// 調(diào)到下一個數(shù)據(jù)項需要走多少步,這在計算rank 的非常有幫助
unsigned int span;
} level[];
} zskiplistNode;
typedef struct zskiplist {
// 跳表頭尾指針
struct zskiplistNode *header, *tail;
// 跳表的長度
unsigned long length;
// 跳表的高度
int level;
} zskiplist;
特別的,在上圖中似乎每個數(shù)據(jù)都被保存了多次,其實只保存了一次。
在 struct zskiplistNode 中數(shù)據(jù)和指針是分開存儲的,struct zskiplistLevel 即是一個描述跳表層級的數(shù)據(jù)結(jié)構(gòu)。我們可以看到,一個節(jié)點(diǎn)主要由有一個存儲真實數(shù)據(jù)的指針,一個后驅(qū)指針,和多個前驅(qū)指針。
TODO 可以在這里插入一張表跳表實際數(shù)據(jù)結(jié)構(gòu)的示意圖。
跳表算法描述如下:找出每一層新插入數(shù)據(jù)位置的前驅(qū)并保存,在 Redis 中跳表插入是根據(jù) score/member 的大?。床欢梢詤⒖?redis ZADD 命令)來決定插入的位置;將新數(shù)據(jù)插入到指定位置,并調(diào)整指針,在 redis 中還會調(diào)整 span。
什么是 span?
http://wiki.jikexueyuan.com/project/redis/images/redis13.png" alt="" />
span 即從兩個相鄰節(jié)點(diǎn)間隔了多少節(jié)點(diǎn)。譬如 level 1,-1 的 span 就是 1;level 2,-1 的 span 為 2。 因為新出入數(shù)據(jù)的層數(shù)是隨機(jī)的,有兩種情況 ① 小于等于原有的層數(shù);② 大于原有的層數(shù)。需要做特殊處理。
1)小于等于原有的層數(shù)
http://wiki.jikexueyuan.com/project/redis/images/redis14.png" alt="" />
redis 中跳表插入算法的具體實現(xiàn):
zskiplistNode *zslInsert(zskiplist *zsl, double score, robj *obj) {
// update 是插入節(jié)點(diǎn)所在位置的前一個節(jié)點(diǎn)。我們在學(xué)習(xí)鏈表插入的時候,需要找到插入
// 位置的前一個節(jié)點(diǎn)。因為在跳表中一個節(jié)點(diǎn)是有多個前驅(qū)指針的,所以這里需要保存的
// 是多個節(jié)點(diǎn),而不是一個節(jié)點(diǎn)
zskiplistNode *update[ZSKIPLIST_MAXLEVEL], *x;
unsigned int rank[ZSKIPLIST_MAXLEVEL];
int i, level;
redisAssert(!isnan(score));
x = zsl->header;
// 遍歷skiplist 中所有的層,找到數(shù)據(jù)將要插入的位置,并保存在update 中
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 &&
compareStringObjects(x->level[i].forward->obj,obj) < 0))) {
rank[i] += x->level[i].span;
x = x->level[i].forward;
}
// update[i] 記錄了新數(shù)據(jù)項的前驅(qū)
update[i] = x;
}
// random 一個level,是隨機(jī)的
/* we assume the key is not already inside, since we allow duplicated
* scores, and the re-insertion of score and redis object should never
* happen since the caller of zslInsert() should test in the hash table
* if the element is already inside or not. */
level = zslRandomLevel();
// random level 比原有的zsl->level 大,需要增加skiplist 的level
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,obj);
for (i = 0; i < level; i++) {
// 新節(jié)點(diǎn)項插到update[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;
}
// 更高的level 尚未調(diào)整span
/* increment span for untouched levels */
for (i = level; i < zsl->level; i++) {
update[i]->level[i].span++;
}
// 調(diào)整新節(jié)點(diǎn)的后驅(qū)指針
x->backward = (update[0] == zsl->header) ? NULL : update[0];
if (x->level[0].forward)
x->level[0].forward->backward = x;
else
zsl->tail = x;
// 調(diào)整skiplist 的長度
zsl->length++;
return x;
}
跳表的刪除算和插入算法步驟類似:找出每一層需刪除數(shù)據(jù)的前驅(qū)并保存;接著調(diào)整指針,在 Redis 中還會調(diào)整 span。
http://wiki.jikexueyuan.com/project/redis/images/redis15.png" alt="" />
Redis 中跳表刪除算法的具體實現(xiàn):
// x 是需要刪除的節(jié)點(diǎn)
// update 是每一個層x 的前驅(qū)數(shù)組
/* Internal function used by zslDelete, zslDeleteByScore and zslDeleteByRank */
void zslDeleteNode(zskiplist *zsl, zskiplistNode *x, zskiplistNode **update) {
int i;
// 調(diào)整span 和forward 指針
for (i = 0; i < zsl->level; i++) {
if (update[i]->level[i].forward == x) {
update[i]->level[i].span += x->level[i].span - 1;
update[i]->level[i].forward = x->level[i].forward;
} else {
// update[i]->level[i].forward == NULL,只調(diào)整span
update[i]->level[i].span -= 1;
}
}
// 調(diào)整后驅(qū)指針
if (x->level[0].forward) {
x->level[0].forward->backward = x->backward;
} else {
zsl->tail = x->backward;
}
// 刪除某一個節(jié)點(diǎn)后,層數(shù)level 可能降低,調(diào)整level
while(zsl->level > 1 && zsl->header->level[zsl->level-1].forward == NULL)
zsl->level--;
// 調(diào)整跳表的長度
zsl->length--;
}
Redis 中結(jié)合跳表(skiplist)和哈希表(dict)形成一個新的數(shù)據(jù)結(jié)構(gòu) zset。添加 dict 是為了快速定位跳表中是否存在某個 member!
typedef struct zset {
dict *dict;
zskiplist *zsl;
} zset;
ZXX 命令是針對有序集合(sorted set)的,譬如:
ZADD
ZCARD
ZCOUNT
ZINCRBY
ZINTERSTORE
ZLEXCOUNT
ZRANGE
ZRANGEBYLEX
ZRANGEBYSCORE
ZRANK
ZREM
ZREMRANGEBYLEX
ZREMRANGEBYRANK
ZREMRANGEBYSCORE
ZREVRANGE
ZREVRANGEBYSCORE
ZREVRANK
ZSCAN
ZSCORE
ZUNIONSTORE
Redis