詳解Redis數(shù)據結構之跳躍表
1、簡介
我們先不談Redis,來看一下跳表。
1.1、業(yè)務場景
場景來自小灰的算法之旅,我們需要做一個拍賣行系統(tǒng),用來查閱和出售游戲中的道具,類似于魔獸世界中的拍賣行那樣,還有以下需求:
拍賣行拍賣的商品需要支持四種排序方式,分別是:按價格、按等級、按剩余時間、按出售者ID排序,排序查詢要盡可能地快。還要支持輸入道具名稱的精確查詢和不輸入名稱的全量查詢。
這樣的業(yè)務場景所需要的數(shù)據結構該如何設計呢?拍賣行商品列表是線性的,最容易表達線性結構的是數(shù)組和鏈表。假如用有序數(shù)組,雖然查找的時候可以使用二分法(時間復雜度O(logN)),但是插入的時間復雜度是O(N),總體時間復雜度是O(N);而如果要使用有序鏈表,雖然插入的時間復雜度是O(1),但是查找的時間復雜度是O(N),總體還是O(N)。
那有沒有一種數(shù)據結構,查找時,有二分法的效率,插入時有鏈表的簡單呢?有的,就是 跳表。
1.2、skiplist
skiplist,即跳表,又稱跳躍表,也是一種數(shù)據結構,用于解決算法問題中的查找問題。
一般問題中的查找分為兩大類,一種是基于各種平衡術,時間復雜度為O(logN),一種是基于哈希表,時間復雜度O(1)。但是skiplist比較特殊,沒有在這里面
2、跳表
2.1、跳表簡介
跳表也是鏈表的一種,是在鏈表的基礎上發(fā)展出來的,我們都知道,鏈表的插入和刪除只需要改動指針就行了,時間復雜度是O(1),但是插入和刪除必然伴隨著查找,而查找需要從頭/尾遍歷,時間復雜度為O(N),如下圖所示是一個有序鏈表(最左側的灰色表示一個空的頭節(jié)點)(圖片來自網絡,以下同):
鏈表中,每個節(jié)點都指向下一個節(jié)點,想要訪問下下個節(jié)點,必然要經過下個節(jié)點,即無法跳過節(jié)點訪問,假設,現(xiàn)在要查找22,我們要先后查找 3->7->11->19->22,需要五次查找。
但是如果我們能夠實現(xiàn)跳過一些節(jié)點訪問,就可以提高查找效率了,所以對鏈表進行一些修改,如下圖:
我們每個一個節(jié)點,都會保存指向下下個節(jié)點的指針,這樣我們就能跳過某個節(jié)點進行訪問,這樣,我們其實是構造了兩個鏈表,新的鏈表之后原來鏈表的一半。
我們姑且稱原鏈表為第一層,新鏈表為第二層,第二層是在第一層的基礎上隔一個取一個。假設,現(xiàn)在還是要查找22,我們先從第二層查找,從7開始,7小于22,再往后,19小于22,再往后,26大于22,所以從節(jié)點19轉到第一層,找到了22,先后查找 7->19->26->22,只需要四次查找。
以此類推,如果再提取一層鏈表,查找效率豈不是更高,如下圖:
現(xiàn)在,又多了第三層鏈表,第三層是在第二層的基礎上隔一個取一個,假設現(xiàn)在還是要查找22,我們先從第三層開始查找,從19開始,19小于22,再往后,發(fā)現(xiàn)是空的,則轉到第二層,19后面的26大于22,轉到第一層,19后面的就是22,先后查找 19->26>22,只需要三次查找。
由上例可見,在查找時,跳過多個節(jié)點,可以大大提高查找效率,skiplist 就是基于此原理。
上面的例子中,每一層的節(jié)點個數(shù)都是下一層的一半,這種查找的過程有點類似二分法,查找的時間復雜度是O(logN),但是例子中的多層鏈表有一個致命的缺陷,就是一旦有節(jié)點插入或者刪除,就會破壞這種上下層鏈表節(jié)點個數(shù)是2:1的結構,如果想要繼續(xù)維持,則需要在插入或者刪除節(jié)點之后,對后面的所有節(jié)點進行一次重新調整,這樣一來,插入/刪除的時間復雜度就變成了O(N)。
2.2、跳表層級之間的關系
如上所述,跳表為了解決插入和刪除節(jié)點時造成的后續(xù)節(jié)點重新調整的問題,引入了隨機層數(shù)的做法。相鄰層數(shù)之間的節(jié)點個數(shù)不再是嚴格的2:1的結構,而是為每個新插入的節(jié)點賦予一個隨機的層數(shù)。下圖展示了如何通過一步步的插入操作從而形成一個跳表:
每一個節(jié)點的層數(shù)都是隨機算法得出的,插入一個新的節(jié)點不會影響其他節(jié)點的層數(shù),因此,插入操作只需要修改插入節(jié)點前后的指針即可,避免了對后續(xù)節(jié)點的重新調整。這是跳表的一個很重要的特性,也是跳表性能明顯由于平衡樹的原因,因為平衡樹在失去平衡之后也需要進行平衡調整。
上圖最后的跳表中,我們需要查找節(jié)點22,則遍歷到的節(jié)點依次是:7->37->19->22,可見,這種隨機層數(shù)的跳表的查找時可能沒有2:1結構的效率,但是卻解決了插入/刪除節(jié)點的問題。
2.3、跳表的復雜度
跳表搜索的時間復雜度平均 O(logN),最壞O(N),空間復雜度O(2N),即O(N)
3、Redis中的跳表
在理解 Redis 的跳躍表之前,我們先回憶一下 Redis 的有序集合(sorted set)操作
- 不重復但有序的字符串元素集合;
- 每個元素均關聯(lián)一個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 中的有序列表的基本操作,我們答題可以看出,在有序列表中,有一個浮點數(shù)作為 score, 當對應一個值,可以根據 score 精確查找和范圍查找,且效率很高
Redis 里面的這種操作的底層實現(xiàn)就是跳表。
上面理解了跳表,再去看 Redis 中的跳表就輕松多了,跳表的實現(xiàn)在 Redis 源碼目錄下 redis.h 文件中
3.1、zskiplistNode
zskiplistNode 表示跳表的一個節(jié)點,聲明如下:
typedef struct zskiplistNode { robj *obj; double score; struct zskiplistNode *backward; struct zskiplistLevel { struct zskiplistNode *forward; unsigned int span; } level[]; } zskiplistNode;
robj 類型是 Redis 中用C語言實現(xiàn)一種集合數(shù)據結構,它可以表示 string、hash、list、set 和 zset 五種數(shù)據類型,這里不做詳細說明,在跳表節(jié)點中,這個類型的指針表示節(jié)點的成員對象
score 表示分值,用于排序和范圍查找
level 是一個柔性數(shù)組,它表示節(jié)點的層級,每層都有一個前進指針 forward,用于指向相同層級指向表尾方向的下一個節(jié)點,而 span 則表示當前節(jié)點在當前層級中距離下一個節(jié)點的跨度,即兩個節(jié)點之間的距離。
初看上去,很容易以為跨度和遍歷節(jié)點有關,實際并不是,遍歷操作只用前進指針就夠了,跨度是用來計算排位(rank)的:在查找某個節(jié)點的過程中,沿途訪問過的所有層的跨度累計起來,就是目標節(jié)點在跳表中的排位。
下圖中,查找成員o3,只經歷了一層,排位為3
在 Redis 中,每個節(jié)點的層級都是根據冪次定律(power law,越大的樹出現(xiàn)的概率越?。╇S機生成的,它是1~32之間的一個數(shù),作為level數(shù)組的大小,即高度
下圖分別展示了三個高度為1、3、5層的節(jié)點
backward 是一個后退指針,每個節(jié)點都有一個,指向當前節(jié)點的表頭方向的下一個節(jié)點,用于從表尾進行遍歷
3.2、zskiplist
zskiplist 表示一個跳表,聲明如下:
typedef struct zskiplist { struct zskiplistNode *header, *tail; unsigned long length; int level; } zskiplist;
header 和 tail 指針分別指向表頭和表尾節(jié)點
length 記錄了節(jié)點數(shù)量
level 記錄了所有節(jié)點中層級最高的節(jié)點的層級,表頭節(jié)點的層高不計算在內
下圖是一個跳表的示例,最左側是一個 zskiplist 結構,其右側是四個 zskiplistNode 節(jié)點,從左向右分別有32層、4層、2層、5層。每個節(jié)點向右的指針即前進指針 forward, BW 則表示后退指針 backward,每個節(jié)點依據節(jié)點的分值 score 進行排列
到此這篇關于Redis數(shù)據結構中的跳躍表的文章就介紹到這了,更多相關Redis數(shù)據結構跳躍表內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
Redis?延時任務實現(xiàn)及與定時任務區(qū)別詳解
這篇文章主要為大家介紹了Redis?延時任務實現(xiàn)及與定時任務區(qū)別詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2023-06-06