redis的跳躍表

跳躍表是一種插入、查詢、刪除的平均時間複雜度爲O(nlogn)的數據結構,在最差狀況下是O(n),固然這幾乎很難出現。

 

和紅黑樹相比較
最差時間複雜度要差不少,紅黑樹是O(nlogn),而跳躍表是O(n)
平均時間複雜度是同樣的
實現要簡單不少

 

維基的跳躍表例子

 

跳躍表的結構如上圖

 

跳躍表的實現仍是一個鏈表,是一個有序的鏈表,在遍歷的時候基於比較,但普通鏈表只能遍歷,跳躍表加入了一個層的概念,層數越高的元素越少,每次先從高層查找,再逐漸降層,直到找到合適的位置。從圖中能夠看到高層的節點遠遠少於底層的節點數,從而實現了跳躍式查找。

 

redis中的定義
/*
 * 跳躍表
 */
typedef struct zskiplist {
    // 表頭節點和表尾節點
    struct zskiplistNode *header, *tail;
    // 表中節點的數量
    unsigned long length;
    // 表中層數最大的節點的層數
    int level;
} zskiplist;
跳躍表的節點
/*
 * 跳躍表節點
 */
typedef struct zskiplistNode {
    // 成員對象
    robj *obj;
    // 分值
    double score;
    // 後退指針
    struct zskiplistNode *backward;
    // 層
    struct zskiplistLevel {
        // 前進指針
        struct zskiplistNode *forward;
        // 跨度
        unsigned int span;
    } level[];
} zskiplistNode;
跳躍表是一個空間換時間的數據結構,和雙鏈表相比,額外的空間開銷就是zskiplistNode中的level數組元素,冗餘存儲了每一層的forward指針。
redis跳躍表實現的一些方法
zslCreateNode
zslCreate
zslFreeNode
zslFree
zslRandomLevel
zslInsert
zslDeleteNode
zslDelete
還有其餘一些
重點關注幾個方法
/*
 * 建立並返回一個新的跳躍表
 *
 * T = O(1)
 */
zskiplist *zslCreate(void) {
    int j;
    zskiplist *zsl;
    // 分配空間
    zsl = zmalloc(sizeof(*zsl));
    // 設置高度和起始層數
    zsl->level = 1;
    zsl->length = 0;
    // 初始化表頭節點
    //表頭必定具備最高的level
    // T = O(1)
    zsl->header = zslCreateNode(ZSKIPLIST_MAXLEVEL,0,NULL);
    for (j = 0; j < ZSKIPLIST_MAXLEVEL; j++) {
        zsl->header->level[j].forward = NULL;
        zsl->header->level[j].span = 0;
    }
    zsl->header->backward = NULL;
    // 設置表尾
    zsl->tail = NULL;
    return zsl;
}
//返回一個隨機值,做爲新跳躍表節點的層次
//層次的合理分佈是跳躍表的效率所在
int zslRandomLevel(void) {
    int level = 1;
    while ((random()&0xFFFF) < (ZSKIPLIST_P * 0xFFFF))
        level += 1;
    return (level<ZSKIPLIST_MAXLEVEL) ? level : ZSKIPLIST_MAXLEVEL;
}
/*
 * 建立一個成員爲 obj ,分值爲 score 的新節點,
 * 並將這個新節點插入到跳躍表 zsl 中。
 * 
 * 函數的返回值爲新節點。
 *
 * T_wrost = O(N^2), T_avg = O(N log N)
 */
zskiplistNode *zslInsert(zskiplist *zsl, double score, robj *obj) {
    zskiplistNode *update[ZSKIPLIST_MAXLEVEL], *x;
    unsigned int rank[ZSKIPLIST_MAXLEVEL];
    int i, level;
    redisAssert(!isnan(score));
    // 在各個層查找節點的插入位置
    // T_wrost = O(N^2), T_avg = O(N log N)
    x = zsl->header;
    for (i = zsl->level-1; i >= 0; i--) {
        /* store rank that is crossed to reach the insert position */
        // 若是 i 不是 zsl->level-1 層
        // 那麼 i 層的起始 rank 值爲 i+1 層的 rank 值
        // 各個層的 rank 值一層層累積
        // 最終 rank[0] 的值加一就是新節點的前置節點的排位
        // rank[0] 會在後面成爲計算 span 值和 rank 值的基礎
        rank[i] = i == (zsl->level-1) ? 0 : rank[i+1];
        // 沿着前進指針遍歷跳躍表
        // T_wrost = O(N^2), T_avg = O(N log N)
        while (x->level[i].forward &&
            (x->level[i].forward->score < score ||
                // 比對分值
                (x->level[i].forward->score == score &&
                // 比對成員, T = O(N)
                compareStringObjects(x->level[i].forward->obj,obj) < 0))) {
            // 記錄沿途跨越了多少個節點
            rank[i] += x->level[i].span;
            // 移動至下一指針
            x = x->level[i].forward;
        }
        // 記錄將要和新節點相鏈接的節點
        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
     * happen since the caller of zslInsert() should test in the hash table
     * if the element is already inside or not. 
     *
     * zslInsert() 的調用者會確保同分值且同成員的元素不會出現,
     * 因此這裏不須要進一步進行檢查,能夠直接建立新元素。
     */
    // 獲取一個隨機值做爲新節點的層數
    // T = O(N)
    level = zslRandomLevel();
    // 若是新節點的層數比表中其餘節點的層數都要大
    // 那麼初始化表頭節點中未使用的層,並將它們記錄到 update 數組中
    // 未來也指向新節點
    if (level > zsl->level) {
        // 初始化未使用層
        // T = O(1)
        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);
    // 將前面記錄的指針指向新節點,並作相應的設置
    // T = O(1)
    for (i = 0; i < level; i++) {
        // 設置新節點的 forward 指針
        x->level[i].forward = update[i]->level[i].forward;
        // 將沿途記錄的各個節點的 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]);
        // 更新新節點插入以後,沿途節點的 span 值
        // 其中的 +1 計算的是新節點
        update[i]->level[i].span = (rank[0] - rank[i]) + 1;
    }
    /* increment span for untouched levels */
    // 未接觸的節點的 span 值也須要增一,這些節點直接從表頭指向新節點
    // T = O(1)
    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;
}

 

跳躍表查找節點的過程(以插入元素爲例,刪除、查找的過程是同樣的)
1.從head開始,根據forward指針向前查找,若是前一個元素大於待查找的元素或者遇到tail指針,下移層次繼續查找;若是下一個元素不大於待查找的元素,forward向前推動一個節點,繼續比較。
2.重複1步驟,直到level1遇到的前一個節點的值大於待查找的值
最終老是能找到比待查找節點的值大的前一個位置,在這個位置插入元素。

 

相關文章
相關標籤/搜索