查詢鍊錶的時間複雜度o(n),即使該鍊錶的是有序的,但若我們在鍊錶上在加一層鏈,且每次跳過乙個節點(即進行一次二分),如下圖所示:
若在l2鏈的基礎上增加一層鏈,在每次跳過l2上的乙個節點(即在l2鏈上進一步二分),那麼我便可以進一步增加搜尋速度,如下圖所示:
此時查詢8只需在l3上搜尋2次,查詢7需要3次(從l3搜尋到節點4,轉至l2搜尋至節點6,轉至l3搜尋至節點7)。
最後再在l3基礎上增加一層鏈,即再進行一次二分,如下圖所示:
這便是理想跳躍表,在鍊錶的基礎上不斷進行二分,共logn層(n為節點個數),查詢的時間為2logn,因為每一層至多進行2次比較。由此可知為什麼說跳躍表的效能可以和平衡樹相媲美。
可是這樣一來為了維護乙個理想跳躍表,刪除和新增時都要調整各層鍊錶,而這十分耗時。因此大多跳躍表都利用概率論實現近似理想跳躍表。
由第一節可知每一層鏈都是在前一次鏈的基礎上進行的二分,每兩個節點鏈入乙個,即鏈入1/2的節點,而這可以通過概率來近似,即每個鏈入li層的節點有1/2的概率鏈入l(i + 1)層。從而可知乙個節點鏈入l1的概率為1,鏈入l2的概率為1/2,鏈入l3的概率為1/4,一次類推1/8,1/16,1/32....。
在程式中如何實現呢?可以採用隨機數的方式,隨機產生[ 1 ~ 2^maxlevel ]的數字,那麼l1概率對應的範圍為1~2^maxlevel,即概率為1;l2概率對應的範圍為2^(maxlevel - 1) ~ 2^maxlevel,即概率為1/2;l3層對應的範圍為2(maxlevel - 2) ~ 2^(maxlevel - 1),即概率為1/4,依次類推,如下圖所示。當資料量較少時不一定十分近似於理想跳躍表,但當資料量較大時根據概率論可知會近似於理想跳躍表。
redis中跳躍表的結構如下所示:
// 跳躍表節點
typedef struct zskiplistnode level;
}zskiplistnode;
typedef struct zskiplist zskiplist;
當建立乙個跳躍表節點時,程式都要根據冪次定律(越大的數出現的概率越小)隨機生成乙個介於1~32之間的值作為level陣列的大小。
【注1】:以上這句是書中給出的,其實就是根據第二節中的方式取出乙個隨機數,從而決定level的大小,也就是要鏈入前幾層鍊錶中。比如,若產生的隨機數在2^(maxlevel - 1)~2^maxlevel 之間則level大小為2,需鏈入l1,l2;若產生的隨機數在2^(maxlevel - 2) ~ 2^(maxlevel - 1)之間則level大小為3,需要鏈入l1,l2,l3。
// 建立跳躍表節點
zskiplistnode *zslcreatenode(int level, double score, robj *obj)
// 建立跳躍表
zskiplist *zslcreate(void)
zsl->header->backward = null;
zsl->tail = null;
return zsl;
}// 釋放跳躍表節點
void zslfreenode(zskiplistnode *node)
// 釋放跳躍表
void zslfree(zskiplist *zsl)
zfree(zsl);
}
int zslrandomlevel(void)
// update[i]記錄了鏈入第i層的哪個節點之後
update[i] = x;
}/* we assume the key is not already inside, since we allow duplicated
* scores, and the re-insertion of score and redis object should never
* if the element is already inside or not. */
level = zslrandomlevel(); // 根據概率計算應該鏈入跳躍表的哪幾層
if (level > zsl->level)
zsl->level = level;
}x = zslcreatenode(level,score,obj); // x指向新增節點
for (i = 0; i < level; i++)
// 對於更高層(為鏈入的層)要更新部分節點的跨度
for (i = level; i < zsl->level; i++)
// 設定後向指標
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;
}
【問】:為什麼不先計算鏈入哪些層,再計算再這些層中的鏈入位置?
【答】:因為更高層也需要計算跨度
在跳躍表中刪除某節點
/* internal function used by zsldelete, zsldeletebyscore and zsldeletebyrank */
// 刪除x指向的節點
void zsldeletenode(zskiplist *zsl, zskiplistnode *x, zskiplistnode **update) else
}if (x->level[0].forward) else
while(zsl->level > 1 && zsl->header->level[zsl->level-1].forward == null)
zsl->level--;
zsl->length--;
}int zsldelete(zskiplist *zsl, double score, robj *obj)
x = x->level[0].forward; // 此時x指向要刪除的節點(原先指向前乙個節點),0層的跨度皆為1
if (x && score == x->score && equalstringobjects(x->obj,obj))
return 0; /* not found */
}
redis原始碼解析 跳躍表
定義 跳躍表是一種有序資料結構,它通過在每個節點中維護多個指向其他節點的指標,從而達到快速訪問節點的目的。跳躍表支援平均o logn 最壞o n 複雜度的節點查詢,大部分情況下,跳躍表的效率可以和平衡樹相媲美,並且因為跳躍表的實現比平衡樹要來得簡單,所以有不少程式都使用跳躍表來替代平衡樹。從圖中可以...
redis原始碼學習之跳躍表
跳躍表對於我來說是乙個比較陌生的資料結構,因此花了一上午的時間先看了一蛤mit的公開課。網易雲課堂 mit跳躍表 什麼是跳躍表,有乙個很簡單的例子,有些地方的火車站跟高鐵站是同乙個站,有的地方只有火車站 假設現在的線路是a b c d e。其中a和c剛剛說的高鐵和火車站在一塊,其他的只有火車站,考慮...
redis zskiplist 跳躍表原始碼學習
typedef struct zskiplistnode level index越大表示層級越高 zskiplistnode typedef struct zskiplist zskiplist typedef struct zset zset 命令 zadd key score value zsk...