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遇到的前一個節點的值大於待查找的值
最終總是能找到比待查找節點的值大的前一個位置,在這個位置插入元素。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM