天天看點

Redis資料結構(四)-跳躍表、跳躍表節點(前進指針、跨度等)概述跳躍表的實作跳躍表節點-zskiplistNode跳躍表-zskiplist跳躍表API跳躍表的查詢跳躍表的插入跳躍表的删除

跳躍表

  • 概述
  • 跳躍表的實作
  • 跳躍表節點-zskiplistNode
    • 層(level)
    • 前進指針(forward)
    • 跨度(span)
    • 後退指針(backward)
    • 分值(score) 和 成員(obj)
  • 跳躍表-zskiplist
  • 跳躍表API
  • 跳躍表的查詢
  • 跳躍表的插入
  • 跳躍表的删除

概述

跳躍表(skiplist) 是一種 有序資料結構,它通過 在每個節點中 維持多個 指向其他節點的指針,進而達到 快速通路節點 的 目的

跳躍表 支援 平均O(logN)、最壞O(N)複雜度的節點查找,還可以通過 順序性操作 來批量 處理節點

在大部分情況下,跳躍表的效率可以和平衡樹相媲美,并且因為跳躍表的實作比平衡樹要來得更為簡單,是以有不少程式都使用跳躍表來代替平衡樹

Redis 使用 跳躍表 作為 有序集合鍵(zset) 的 底層實作之一,如果一個有序集合包含的元素數量比較多,又或者有序集合中元素的成員(member)是比較長的字元串時,Redis就會使用跳躍表來作為有序集合鍵的底層實作

和連結清單、字典等資料結構被廣泛地應用在Redis内部不同,Redis隻在兩個地方用到了跳躍表,一個是 實作 有序集合鍵(zset),另一個是 在叢集節點中 用作 内部資料結構,除此之外,跳躍表在Redis裡面沒有其他用途

跳躍表的實作

Redis的跳躍表由

redis.h/zskiplistNode

redis.h/zskiplist

兩個結構定義

zskiplistNode

結構 用于表示 跳躍表節點

zskiplist

結構 則用于 儲存 跳躍表節點 的 相關資訊,比如節點的數量,以及指向表頭節點和表尾節點的指針等等

Redis資料結構(四)-跳躍表、跳躍表節點(前進指針、跨度等)概述跳躍表的實作跳躍表節點-zskiplistNode跳躍表-zskiplist跳躍表API跳躍表的查詢跳躍表的插入跳躍表的删除

上圖展示了一個跳躍表示例,位于圖檔最左邊的是zskiplist結構,該結構包含以下屬性:

  • header:指向 跳躍表 的 表頭節點
  • tail:指向 跳躍表 的 表尾節點
  • level:記錄目前跳躍表内,層數最大 的 那個節點 的 層數(表頭節點的層數不計算在内)
  • length:記錄跳躍表的長度,也即是,跳躍表目前包含節點的數量(表頭節點不計算在内)

位于zskiplist結構右方的是四個zskiplistNode結構,該結構包含以下屬性:

  • 層(level):節點中用L1、L2、L3等字樣标記節點的各個層,L1代表第一層,L2代表第二層,以此類推。每個層都帶有兩個屬性:前進指針和跨度。前進指針 用于 通路 位于 表尾方向 的 其他節點,而 跨度 則記錄了 前進指針 所指的向節點 和 目前節點 的 距離。在上面的圖檔中,連線上帶有數字的箭頭就代表前進指針,而那個數字就是跨度。當程式從表頭向表尾進行周遊時,通路會沿着層的前進指針進行
  • 後退(backward)指針:節點中用

    BW

    字樣标記節點的後退指針,它指向 位于目前節點 的 前一個節點。後退指針在程式從表尾向表頭周遊時使用
  • 分值(score):各個節點中的1.0、2.0和3.0是節點所儲存的分值。在跳躍表中,節點按各自所儲存的分值從小到大排列
  • 成員對象(obj):各個節點中的o1、o2和o3是節點所儲存的成員對象

注意表頭節點和其他節點的構造是一樣的:表頭節點也有後退指針、分值和成員對象,不過表頭節點的這些屬性都不會被用到,是以圖中省略了這些部分,隻顯示了表頭節點的各個層

跳躍表節點-zskiplistNode

跳躍表節點的實作由

redis.h/zskiplistNode

結構定義

typedef struct zskiplistNode {
    // 層
    struct zskiplistLevel {
        // 前進指針
        struct zskiplistNode *forward;
        // 跨度
      unsigned int span;
    } level[];
    // 後退指針
    struct zskiplistNode *backward;
    // 分值
    double score;
    // 成員對象
    robj *obj;
} zskiplistNode;
           

層(level)

跳躍表節點 的 level數組 可以包含 多個元素,每個元素 都包含一個 指向 其他節點 的 指針

程式可以通過這些層 來加快 通路其他節點的速度,一般來說,層的數量越多,通路其他節點的速度就越快

每次建立一個新跳躍表節點的時候,程式都根據幂次定律(power law,越大的數出現的機率越小)随機生成一個介于1和32之間的值 作為 level數組的大小,這個大小就是層的“高度”

前進指針(forward)

每個層 都有一個 指向 表尾方向 的 前進指針(level[i].

forward

屬性),用于 從表頭 向 表尾方向 通路節點

Redis資料結構(四)-跳躍表、跳躍表節點(前進指針、跨度等)概述跳躍表的實作跳躍表節點-zskiplistNode跳躍表-zskiplist跳躍表API跳躍表的查詢跳躍表的插入跳躍表的删除

以上圖為例,周遊跳躍表中所有節點的路徑如下:

  • 疊代程式首先通路跳躍表的第一個節點(表頭),然後從第四層的前進指針移動到表中的第二個節點
  • 在第二個節點時,程式沿着第二層的前進指針移動到表中的第三個節點
  • 在第三個節點時,程式同樣沿着第二層的前進指針移動到表中的第四個節點
  • 當程式再次沿着第四個節點的前進指針移動時,它碰到一個NULL,程式知道這時已經到達了跳躍表的表尾,于是結束這次周遊

跨度(span)

層的跨度(level[i].

span

屬性)用于記錄兩個節點之間的距離:

  • 兩個節點之間的跨度越大,它們相距得就越遠
  • 指向NULL的所有前進指針的跨度都為0,因為它們沒有連向任何節點

初看上去,很容易以為跨度和周遊操作有關,但實際上并不是這樣,周遊操作隻使用前進指針就可以完成了,跨度 實際上 是 用來計算排位(rank)的

在查找某個節點的過程中,将 沿途 通路過的所有層 的 跨度(span) 累加起來,得到的結果 就是 目标節點 在 跳躍表中的排位

後退指針(backward)

節點的後退指針(

backward

屬性) 用于 從表尾 向 表頭方向 通路 節點

跟可以一次跳過多個節點的前進指針不同,因為每個節點隻有一個後退指針,是以每次隻能後退至前一個節點

分值(score) 和 成員(obj)

節點的分值(

score

屬性)是一個double類型的浮點數,跳躍表中的所有節點都按分值從小到大來排序

節點的成員對象(obj屬性)是一個指針,它指向一個字元串對象,而字元串對象則儲存着一個SDS值

在同一個跳躍表中,各個節點儲存的成員對象必須是唯一的,但是 多個節點 儲存的分值 卻可以是相同的

分值相同的節點 将按照 成員對象 在 字典序中的大小 來進行排序,成員對象較小的節點會排在前面(靠近表頭的方向),而成員對象較大的節點則會排在後面(靠近表尾的方向)

跳躍表-zskiplist

僅靠 多個 跳躍表節點 就可以 組成一個跳躍表

但通過使用一個

zskiplist

結構來持有這些節點,程式可以更友善地對整個跳躍表進行處理,比如快速通路跳躍表的表頭節點和表尾節點,或者快速地擷取跳躍表節點的數量(也即是跳躍表的長度)等資訊

zskiplist結構的定義如下:

typedef struct zskiplist {
    // 表頭節點和表尾節點
    structz skiplistNode *header, *tail;
    // 表中節點的數量
    unsigned long length;
    // 表中 層數最大的節點 的 層數
    int level;
} zskiplist;
           

header和tail指針分别指向跳躍表的表頭和表尾節點,通過這兩個指針,程式定位表頭節點和表尾節點的複雜度為O(1)

通過使用length屬性來記錄節點的數量,程式可以在O(1)複雜度内傳回跳躍表的長度

level屬性則用于在O(1)複雜度内 擷取 跳躍表中 層高最大 的 那個節點 的 層數量,注意表頭節點的層高并不計算在内

跳躍表API

Redis資料結構(四)-跳躍表、跳躍表節點(前進指針、跨度等)概述跳躍表的實作跳躍表節點-zskiplistNode跳躍表-zskiplist跳躍表API跳躍表的查詢跳躍表的插入跳躍表的删除
Redis資料結構(四)-跳躍表、跳躍表節點(前進指針、跨度等)概述跳躍表的實作跳躍表節點-zskiplistNode跳躍表-zskiplist跳躍表API跳躍表的查詢跳躍表的插入跳躍表的删除

跳躍表的查詢

跳躍表的查詢 是從 頂層 往下找,那麼會先從第K層開始找,方式就是循環比較,如果K層節點的下一個節點為空 說明 到達末尾,會跳到第二層,繼續周遊,直到找到對應節點

跳躍表的插入

先确定 要插入的元素要 占據的層數 K(采用丢硬币的方式,這完全是随機的)

然後在 Level 1 … Level K 各個層的連結清單都插入元素

如果 K 超過 連結清單現有的層數,則要添加新的層

跳躍表的删除

跳躍表的删除和查找類似,都是一級一級找到相對應的節點,然後 将 要删除節點的 前一個節點的 forward 指向 下一個節點,完全和連結清單類似

繼續閱讀