Grape
所有視頻:https://segmentfault.com/a/11...面試
你們想象一下下面這種場景:redis
面試官:咱們有一個有序的數組2,5,6,7,9,咱們要去查7,設計一個算法。 考生:第一眼看到相信你們都會看出來是二分查找,O(logN)就完事了。 面試官:那麼接下來咱們把這個數組換成鏈表呢(2->5->6->7->9)? 考生:這簡單,二叉樹,一樣logN。 面試官:那麼請手寫一下完整代碼! 考生:卒
想象一下,給你一張草稿紙,一隻筆,一個編輯器,你能當即實現一顆紅黑樹,或者AVL樹出來嗎? 很難吧,這須要時間,要考慮不少細節,要參考一堆算法與數據結構之類的樹,還要參考網上的代碼,至關麻煩。算法
回去以後,小明很難過,又不想被二叉樹所折磨,想要找一個方法來代替二叉樹,在他的不懈努力之下,終於,找出了替代紅黑樹的方法,它叫作skiplist。segmentfault
怎麼解決的呢?
首先,表是處於一個初始狀態的,沒有任何一個元素,相似於下圖:
那麼,咱們繼續插入一個元素2,那麼它就變成了這樣。
而後咱們拋硬幣,結果是正面,那麼咱們要將2插入到L2層,以下圖:
繼續拋硬幣,結果是反面,那麼元素2的插入操做就中止了,插入後的表結構就是上圖所示。接下來,咱們插入元素5,跟元素2的插入同樣,如今L1層插入5,以下圖:
接下來繼續拋硬幣,是正面的話就上升一層,不然就終止,繼續插入其餘新的元素。
那麼最後,咱們建形成的樣子就以下圖所示。
這樣子就構形成了skiplist。固然由於規模小,結果極可能不是一個理想的跳躍表。可是若是元素個數n的規模很大,學過幾率論的同窗都知道,最終的表結構確定很是接近於理想跳躍表。
這樣是否是很簡單?
迴歸正題,咱們如何查找到6呢?很簡單,咱們看首先和6比較,發現7大於6,咱們就向後走,發現相等就找到了節點7.固然,若是咱們找5的話就是和6比完以後降到L2,而後和2比,比2大比6小,繼續降級,找到5。
小明同窗是一個很會觸類旁通的人,既然都知道查找這麼簡單了,就看看插入吧,等把增刪改查都解決了,媽媽就不再用擔憂個人紅黑樹了。數組
接下來咱們就看看插入,咱們要插入一個4,怎麼辦呢?
從最高層開始找到每一層比4大的節點的前一個值,而後投硬幣,隨機選擇層數後插入,舉個例子這個值爲4.那麼插入以後就是下圖所示。數據結構
咱們發現,他會新增一層,而且會在同層級之間進行鏈接。而後就完成了插入操做。編輯器
刪除操做:
刪除操做相似於插入操做,包含以下3步:一、查找到須要刪除的結點 二、刪除結點 三、調整指針。性能
到此,Skiplist的增刪改查就很明確了,可是知其然咱們也得知其因此然,小明同窗不拋棄不放棄,想要知道他是怎麼樣實現的,以及在上邊過程當中本身的問題。學習
1. 爲何要投硬幣?
咱們先解釋一下投硬幣這個流程:跳躍表節點的層數限制在了64(在redis5.0以前是32),若想超過64層得連續64次拋硬幣都獲得正面,這得有足夠多的節點,redis限定了拋硬幣正面的機率爲1/4,因此到達64層的機率爲(1/2)^128,通常一臺64位的計算機能擁有的最大內存也沒法存儲這麼多zskiplistNode,因此對於基本使用 64層的上限已經足夠高了,再高也不必 浪費頭節點的內存。因此,投硬幣是爲了讓數據儘可能都在低的層級以達到節省內存的目的。spa
2. 跳躍表是什麼?在哪用?
跳躍表( skiplist) 是一種有序的數據結構, 它經過在每一個節點中維持多個指向其餘節點的指針,從而達到快速訪問節點的目的。跳躍表支持平均O(logN),最壞O(N)複雜度的節點查找. 大部分狀況下,跳躍表的效率能夠和平衡樹想媲美,而且跳躍表的實現比平衡樹更爲簡單。
Redis 使用跳躍表做爲有序集合鍵的底層實現之一, 若是一個有序集合包含的元素數量較多,或者有序集合中元素的成員是比較長的字符串, Redis 會使用跳躍表來做爲有序集合的底層實現。
那跳錶這麼棒在Redis中用到的地方確定很是多嗎?答案是否認的,Redis 只在兩個地方用到了跳躍表,一個是實現有序集合鍵,另外一個是在集羣節點中用做內部數據結構, 除此以外,跳躍表在 Redis 中沒有其餘用途。
3. 跳躍表是怎麼實現的?
咱們來看一看skiplist的源碼:
typedef struct zskiplistNode { sds ele; //元素 double score; //分值 struct zskiplistNode *backward; //後退指針,後退指針用於從表尾向表頭訪問節點,跟能夠一次跳過多個節點的前進指針不一樣,每一個節點只有一個後退指針 struct zskiplistLevel { struct zskiplistNode *forward; //前進指針,每一個層都有一個指向表尾方向的指針.用於從表頭向表尾方向訪問節點 unsigned long span; //跨度,層的跨度用於記錄兩個節點之間的距離. 兩個節點之間的跨度越大,它們距離越遠;指向 NULL 的節點的跨度爲0 } level[]; } zskiplistNode; //跳躍表的 level 數組能夠包含多個元素,每一個元素都包含一個指向其餘節點的指針,程序能夠經過這些指針加快訪問速度 //通常來講,層的數量越多,訪問其餘節點的速度越快 //每次建立一個新跳躍表節點時,程序會根據冪次定律(越大的數出現的機率越小)隨機生成一個介於1 和 64 之間的值做爲 level 數組的大小,這個大小就是層的高度 typedef struct zskiplist { struct zskiplistNode *header, *tail; //表頭和表尾指針 unsigned long length; //節點的數量 int level; //層數最大的節點的層數 } zskiplist;
由此咱們能夠得出skiplist內存結構圖以下:
抽象內存結構圖以下:
另外呢? 咱們在gdb有序集合zset代碼的時候,發現程序會在建立skiplist的以前會先建立一個字典dict。那麼,這個dict的做用是什麼呢?dict的做用呢是一個hashtable,用來映射元素與zset中分值score的關係。擁有這個映射表,咱們去查找一個元素的分值時間複雜度就變成了O(1)。
4. redis使用跳躍表而不是平衡樹的緣由
skiplist和各類平衡樹(如AVL、紅黑樹等)的元素是有序排列的,而哈希表不是有序的。所以,在哈希表上只能作單個key的查找,不適宜作範圍查找。所謂範圍查找,指的是查找那些大小在指定的兩個值之間的全部節點。
在作範圍查找的時候,平衡樹比skiplist操做要複雜。在平衡樹上,咱們找到指定範圍的小值以後,還須要以中序遍歷的順序繼續尋找其它不超過大值的節點。若是不對平衡樹進行必定的改造,這裏的中序遍歷並不容易實現。而在skiplist上進行範圍查找就很是簡單,只須要在找到小值以後,對第1層鏈表進行若干步的遍歷就能夠實現。
平衡樹的插入和刪除操做可能引起子樹的調整,邏輯複雜,而skiplist的插入和刪除只須要修改相鄰節點的指針,操做簡單又快速。
從內存佔用上來講,skiplist比平衡樹更靈活一些。通常來講,平衡樹每一個節點包含2個指針(分別指向左右子樹),而skiplist每一個節點包含的指針數目平均爲1/(1-p),具體取決於參數p的大小。若是像Redis裏的實現同樣,取p=1/4,那麼平均每一個節點包含1.33個指針,比平衡樹更有優點。
查找單個key,skiplist和平衡樹的時間複雜度都爲O(log n),大致至關;而哈希表在保持較低的哈希值衝突機率的前提下,查找時間複雜度接近O(1),性能更高一些。因此咱們日常使用的各類Map或dictionary結構,大都是基於哈希表實現的。
從算法實現難度上來比較,skiplist比平衡樹要簡單得多。
最後,學以至用,知道skiplist是怎麼回事,咱們還須要知道它的老東家在怎麼用它,你們能夠想一下Redis中的ZADD,ZRANGE,ZRANGEBYSCORE等命令是怎麼用到它的。
若是想要了解有關跳躍表源碼更具體的分析,建議閱讀【Redis學習筆記】2018-05-29 redis源碼學習之跳躍表。