Redis數據結構之跳躍表

一、簡介

咱們先不談Redis,來看一下跳錶。node

1.一、業務場景

場景來自小灰的算法之旅,咱們須要作一個拍賣行系統,用來查閱和出售遊戲中的道具,相似於魔獸世界中的拍賣行那樣,還有如下需求:python

  1. 拍賣行拍賣的商品須要支持四種排序方式,分別是:按價格、按等級、按剩餘時間、按出售者ID排序,排序查詢要儘量地快。
  2. 還要支持輸入道具名稱的精確查詢和不輸入名稱的全量查詢。

這樣的業務場景所須要的數據結構該如何設計呢?拍賣行商品列表是線性的,最容易表達線性結構的是數組和鏈表。假如用有序數組,雖然查找的時候可使用二分法(時間複雜度O(logN)),可是插入的時間複雜度是O(N),整體時間複雜度是O(N);而若是要使用有序鏈表,雖然插入的時間複雜度是O(1),可是查找的時間複雜度是O(N),整體仍是O(N)。mysql

那有沒有一種數據結構,查找時,有二分法的效率,插入時有鏈表的簡單呢?有的,就是 跳錶redis

1.二、skiplist

skiplist,即跳錶,又稱跳躍表,也是一種數據結構,用於解決算法問題中的查找問題。算法

通常問題中的查找分爲兩大類,一種是基於各類平衡術,時間複雜度爲O(logN),一種是基於哈希表,時間複雜度O(1)。可是skiplist比較特殊,沒有在這裏面sql

二、跳錶

2.一、跳錶簡介

跳錶也是鏈表的一種,是在鏈表的基礎上發展出來的,咱們都知道,鏈表的插入和刪除只須要改動指針就好了,時間複雜度是O(1),可是插入和刪除必然伴隨着查找,而查找須要從頭/尾遍歷,時間複雜度爲O(N),以下圖所示是一個有序鏈表(最左側的灰色表示一個空的頭節點)(圖片來自網絡,如下同):mongodb

image-20201110214951860

鏈表中,每一個節點都指向下一個節點,想要訪問下下個節點,必然要通過下個節點,即沒法跳過節點訪問,假設,如今要查找22,咱們要前後查找 3->7->11->19->22,須要五次查找。數組

可是若是咱們可以實現跳過一些節點訪問,就能夠提升查找效率了,因此對鏈表進行一些修改,以下圖:網絡

image-20201110215429303

咱們每一個一個節點,都會保存指向下下個節點的指針,這樣咱們就能跳過某個節點進行訪問,這樣,咱們實際上是構造了兩個鏈表,新的鏈表以後原來鏈表的一半。數據結構

咱們姑且稱原鏈表爲第一層,新鏈表爲第二層,第二層是在第一層的基礎上隔一個取一個。假設,如今仍是要查找22,咱們先從第二層查找,從7開始,7小於22,再日後,19小於22,再日後,26大於22,因此從節點19轉到第一層,找到了22,前後查找 7->19->26->22,只須要四次查找。

以此類推,若是再提取一層鏈表,查找效率豈不是更高,以下圖:

image-20201110220408220

如今,又多了第三層鏈表,第三層是在第二層的基礎上隔一個取一個,假設如今仍是要查找22,咱們先從第三層開始查找,從19開始,19小於22,再日後,發現是空的,則轉到第二層,19後面的26大於22,轉到第一層,19後面的就是22,前後查找 19->26>22,只須要三次查找。

由上例可見,在查找時,跳過多個節點,能夠大大提升查找效率,skiplist 就是基於此原理。

上面的例子中,每一層的節點個數都是下一層的一半,這種查找的過程有點相似二分法,查找的時間複雜度是O(logN),可是例子中的多層鏈表有一個致命的缺陷,就是一旦有節點插入或者刪除,就會破壞這種上下層鏈表節點個數是2:1的結構,若是想要繼續維持,則須要在插入或者刪除節點以後,對後面的全部節點進行一次從新調整,這樣一來,插入/刪除的時間複雜度就變成了O(N)。

2.二、跳錶層級之間的關係

如上所述,跳錶爲了解決插入和刪除節點時形成的後續節點從新調整的問題,引入了隨機層數的作法。相鄰層數之間的節點個數再也不是嚴格的2:1的結構,而是爲每一個新插入的節點賦予一個隨機的層數。下圖展現瞭如何經過一步步的插入操做從而造成一個跳錶:

image-20201110220408220

每個節點的層數都是隨機算法得出的,插入一個新的節點不會影響其餘節點的層數,所以,插入操做只須要修改插入節點先後的指針便可,避免了對後續節點的從新調整。這是跳錶的一個很重要的特性,也是跳錶性能明顯因爲平衡樹的緣由,由於平衡樹在失去平衡以後也須要進行平衡調整。

上圖最後的跳錶中,咱們須要查找節點22,則遍歷到的節點依次是:7->37->19->22,可見,這種隨機層數的跳錶的查找時可能沒有2:1結構的效率,可是卻解決了插入/刪除節點的問題。

2.三、跳錶的複雜度

跳錶搜索的時間複雜度平均 O(logN),最壞O(N),空間複雜度O(2N),即O(N)

三、Redis中的跳錶

在理解 Redis 的跳躍表以前,咱們先回憶一下 Redis 的有序集合(sorted set)操做

  • 不重複但有序的字符串元素集合;
  • 每一個元素均關聯一個double類型的score,Redis 根據score進行從小到大排序;
  • score能夠重複,重複的按照插入順序進行排序;

示例以下:

redis 127.0.0.1:6379> ZADD runoobkey 1 redis
(integer) 1
redis 127.0.0.1:6379> ZADD runoobkey 2 mongodb
(integer) 1
redis 127.0.0.1:6379> ZADD runoobkey 3 mysql
(integer) 1
redis 127.0.0.1:6379> ZADD runoobkey 3 mysql
(integer) 0
redis 127.0.0.1:6379> ZADD runoobkey 4 mysql
(integer) 0
redis 127.0.0.1:6379> ZRANGE runoobkey 0 10 WITHSCORES

"redis"
"1"
"mongodb"
"2"
"mysql"
"4"

這個是 Redis 中的有序列表的基本操做,咱們答題能夠看出,在有序列表中,有一個浮點數做爲 score, 當對應一個值,能夠根據 score 精確查找和範圍查找,且效率很高

Redis 裏面的這種操做的底層實現就是跳錶。

上面理解了跳錶,再去看 Redis 中的跳錶就輕鬆多了,跳錶的實如今 Redis 源碼目錄下 redis.h 文件中

3.一、zskiplistNode

zskiplistNode 表示跳錶的一個節點,聲明以下:

typedef struct zskiplistNode {
    robj *obj;
    double score;
    struct zskiplistNode *backward;
    struct zskiplistLevel {
        struct zskiplistNode *forward;
        unsigned int span;
    } level[];
} zskiplistNode;

robj 類型是 Redis 中用C語言實現一種集合數據結構,它能夠表示 string、hash、list、set 和 zset 五種數據類型,這裏不作詳細說明,在跳錶節點中,這個類型的指針表示節點的成員對象

score 表示分值,用於排序和範圍查找

level 是一個柔性數組,它表示節點的層級,每層都有一個前進指針 forward,用於指向相同層級指向表尾方向的下一個節點,而 span 則表示當前節點在當前層級中距離下一個節點的跨度,即兩個節點之間的距離。

初看上去,很容易覺得跨度和遍歷節點有關,實際並非,遍歷操做只用前進指針就夠了,跨度是用來計算排位(rank)的:在查找某個節點的過程當中,沿途訪問過的全部層的跨度累計起來,就是目標節點在跳錶中的排位。

下圖中,查找成員o3,只經歷了一層,排位爲3

image-20201110232105792

在 Redis 中,每一個節點的層級都是根據冪次定律(power law,越大的樹出現的機率越小)隨機生成的,它是1~32之間的一個數,做爲level數組的大小,即高度

下圖分別展現了三個高度爲一、三、5層的節點

image-20201110231806856

backward 是一個後退指針,每一個節點都有一個,指向當前節點的表頭方向的下一個節點,用於從表尾進行遍歷

3.二、zskiplist

zskiplist 表示一個跳錶,聲明以下:

typedef struct zskiplist {
    struct zskiplistNode *header, *tail;
    unsigned long length;
    int level;
} zskiplist;

headertail 指針分別指向表頭和表尾節點

length 記錄了節點數量

level 記錄了全部節點中層級最高的節點的層級,表頭節點的層高不計算在內

下圖是一個跳錶的示例,最左側是一個 zskiplist 結構,其右側是四個 zskiplistNode 節點,從左向右分別有32層、4層、2層、5層。每一個節點向右的指針即前進指針 forward, BW 則表示後退指針 backward,每一個節點依據節點的分值 score 進行排列

image-20201110232246288

相關文章
相關標籤/搜索