1. 程式人生 > 其它 >為實習準備的資料結構(9)-- 跳錶

為實習準備的資料結構(9)-- 跳錶

技術標籤:為實習準備的資料結構資料結構redis演算法跳錶C++

在這裡插入圖片描述

文章目錄

跳錶

讓你現場手寫一棵紅黑樹、AVL樹、伸展樹之類的,你行嗎?
要不讓我查資料,我估計只能扯皮。

跳錶就不一樣了,看懂它的原理很簡單,根據它的原理直接手寫也是可以實現的。

為什麼?
跳錶(skip list) 對應的是平衡樹(AVL Tree),是一種 插入/刪除/搜尋 都是 O(log n) 的資料結構。它最大的優勢是原理簡單容易實現方便擴充套件、效率更高。因此在一些熱門的專案裡用來替代平衡樹,如 redis, leveldb 等。

跳錶是一個隨機化的資料結構,實質就是一種可以進行二分查詢的有序連結串列。跳錶在原有的有序連結串列上面增加了多級索引,通過索引來實現快速查詢。

連結串列會寫不?連結串列不會寫的話,可以走了。

開頭那張圖看著有感覺嗎?我第一眼看到那個圖,大概就明白了,同時覺得,秀啊!!!
還能這麼玩。

一個節點要不要被索引,建幾層的索引,都在節點插入時由拋硬幣決定。當然,雖然索引的節點、索引的層數是隨機的,為了保證搜尋的效率,要大致保證每層的節點數目與上節的結構相當(差不多對半開)。

在這裡插入圖片描述


跳錶的搜尋

在這裡插入圖片描述

我現在要在這裡面搜尋“17”,具體流程是如何的呢?

為了加快搜索,需要從最高層4開始搜尋(因為最高層的節點少,容易定位)
,首先檢視6節點,發現17比其大,向後搜尋,發現6後面的節點指向了Nil(4),那麼搜尋的層數降低1層, 從此節點的第3層開始搜尋,發現下個節點是25,大於17,那麼再降低一層,從2層開始搜尋,發現第2層是9,小於17,繼續搜尋,發現9節點的下一個數是17,搜尋完成。總共查詢了 4次,完成搜尋(不包含Nil節點的訪問。),這種情況下普通有序連結串列需要6次訪問。可以設想下,如果層數為1層的話,那麼此時跳錶為最壞的情況,退化成有序單鏈表。複雜度O(n)

跳錶的插入

第一步:找到待插入節點該去的位置。
第二步:確定該元素要佔據的層數 K(採用丟硬幣的方式,這完全是隨機的)。
第三步:在 Level 1 … Level K 各個層的連結串列都插入元素。

第四步:用Update陣列記錄插入位置,同樣從頂層開始,逐層找到每層需要插入的位置,再生成層數並插入。
(這個update陣列不知道的話先存疑)


拋硬幣

拋硬幣,如果是正面(random() < p)則層數加一,直到丟擲反面為止。設定一個 MaxLevel,防止如果運氣太好,層數就會太高,而太高的層數往往並不會提供額外的效能。


跳錶的刪除

刪除的時候和插入相同,都是先搜尋。唯一注意的一點是刪除的節點也許要修改skiplist->length的值。

銷燬整個調錶的時候,從level=1銷燬即可,別忘記釋放head和skiplist。

這裡就不過多的放圖了,意會一下子。

大年初一還要搞程式碼,哎。


跳錶的程式碼實現

跳錶資料結構

如上圖中的E節點,表示的是頭節點,一般跳錶的實現,最大有多少層(MAX_LEVEL)是確定的。所以e的個數是固定的。

#define SKIPLIST_MAXLEVEL 32
#define ZSKIPLIST_P 0.25

typedef struct skiplistNode_t{
    void* value;
    double score;
    struct skiplistNode_t* forward[];
}skiplistNode;

typedef struct skiplist{
    skiplistNode* head;
    int level;
    uint32_t length;
}skiplist;

初始化跳錶

skiplistNode* createNode(int level,void* value,double score)
{
    skiplistNode *slNode = (skiplistNode*)malloc(sizeof(*slNode)+level*sizeof(skiplistNode));
    if(!slNode)
    {
        return NULL;
    }
    slNode->value = value;
    slNode->score = score;
    return slNode;
}

skiplist* slCreate(void)
{
    int i;
    skiplist* sl = (skiplist*)malloc(sizeof(*sl));
    if(!sl)
    {
        return NULL;
    }
    sl->length = 0;
    sl->level = 1;
    sl->tail = NULL;
    sl->head = createNode(SKIPLIST_MAXLEVEL,NULL,0.0);
    for(i=0;i<SKIPLIST_MAXLEVEL;i++)
    {
        sl->head->forward[i] = NULL;
    }
    return sl;
}

插入節點

插入的時候,會呼叫一個randomLevel的函式,他可以概率返回1~MAX_LEVEL之間的值,但是level的值越大,概率越小。

skiplistNode *slInsert(skiplist *sl, void* value, double score)
{
    skiplistNode* sn,*update[SKIPLIST_MAXLEVEL];
    int i, level;
    sn = sl->head;
    for(i=sl->level-1;i>=0;i--)
    {
        while(sn->forward[i]&&(sn->forward[i]->score<score)){
            sn = sn->forward[i];
        }
        update[i] = sn;
    }
    if(sn->forward[0]&&sn->forward[0]->score == score)
    {
        printf("insert failed,exist!!\n");
        return NULL;
    }

    level = slRandomLevel();
    printf("score:%.2lf level:%d\n",score,level);
    if(level>sl->level){
        for(i=sl->level;i<level;i++)
        {
            update[i] = sl->head;
        }
        sl->level = level;
    }
    sn = createNode(level,value,score);
    for(i=0;i<level;i++)
    {
        sn->forward[i] = update[i]->forward[i];
        update[i]->forward[i] = sn;
    }
    sl->length++;
    return sn;
}

刪除節點

int slDelete(skiplist *sl, double score)
{
    skiplistNode *update[SKIPLIST_MAXLEVEL];
    skiplistNode *sn;
    int i;
    sn = sl->head;
    for(i=sl->level-1;i>=0;i--)
    {
        while(sn->forward[i]&&sn->forward[i]->score<score){
            sn = sn->forward[i];
        }
        update[i] = sn;
    }
    sn = sn->forward[0];
    if(sn->score != score)
    {
        return -1;
    }
    for(i=0;i<sl->level;i++)
    {
        if(update[i]->forward[i] != sn){
            break;
        }
        update[i]->forward[i] = sn->forward[i];
    }
    free(sn);
    while(sl->level>1&&sl->head->forward[sl->level-1] == NULL){
        sl->level--;
    }
    sl->length--;
    return 0;
}

銷燬跳錶

銷燬整個調錶的時候,從level=1銷燬即可,別忘記釋放head和skiplist。

void slFree(skiplist *sl)
{
    skiplistNode* sn = sl->head,*st;
    st = sn->forward[0];
    free(sn);
    while(st)
    {
        sn = st->forward[0];
        free(st);
        st = sn;
    }
    free(sl);
}

接下來我們看點其它的,碧如說跳錶與redis。(我第一次接觸跳錶也是在redis的原始碼中)


為什麼Redis要用跳錶來實現有序集合?

效能. 主要是對標AVL. 但是AVL實現複雜,對於頻繁的增刪操作極大可能造成子樹的平衡操作,這樣很明顯就會浪費效能。

請看開發者說的,他為什麼選用skiplist The Skip list:

There are a few reasons:1) They are not very memory intensive. It’s up to you basically. Changing parameters about the probability of a node to have a given number of levels will make then less memory intensive than btrees.2) A sorted set is often target of many ZRANGE or ZREVRANGE operations, that is, traversing the skip list as a linked list. With this operation the cache locality of skip lists is at least as good as with other kind of balanced trees.3) They are simpler to implement, debug, and so forth. For instance thanks to the skip list simplicity I received a patch (already in Redis master) with augmented skip lists implementing ZRANK in O(log(N)). It required little changes to the code.About the Append Only durability & speed, I don’t think it is a good idea to optimize Redis at cost of more code and more complexity for a use case that IMHO should be rare for the Redis target (fsync() at every command). Almost no one is using this feature even with ACID SQL databases, as the performance hint is big anyway.About threads: our experience shows that Redis is mostly I/O bound. I’m using threads to serve things from Virtual Memory. The long term solution to exploit all the cores, assuming your link is so fast that you can saturate a single core, is running multiple instances of Redis (no locks, almost fully scalable linearly with number of cores), and using the “Redis Cluster” solution that I plan to develop in the future.


在併發環境下skiplist有另外一個優勢,紅黑樹在插入和刪除的時候可能需要做一些rebalance的操作,這樣的操作可能會涉及到整個樹的其他部分,而skiplist的操作顯然更加區域性性一些,鎖需要盯住的節點更少,因此在這樣的情況下效能好一些。


大過年的,祝大家新年快樂哦!!!
在這裡插入圖片描述
在這裡插入圖片描述