[redis] 數據結構 -- 跳躍表

skiplist

  1. skiplist 是有序集合底層實現之一,另外一個是 ziplist。同時滿足以下條件時使用 ziplist

     		1. 元素數量小於 128 個,可以通過 zset-max-ziplist-entries 來修改
     		2. 所有 member 的長度都小於 64 字節,可以通過  zset-max-ziplist-value 來修改
    

    skiplist 插入、刪除、查找的複雜度均爲O(logN)

  2. 結構

    	// 跳躍表節點
    	typeof struct zskiplistNode {
    		// 層,每次創建層隨機產生 1 到 32 之間的值作爲 level 的大小(高度)
    		struct zskiplistLevel {
    			// 前進指針,用於從表頭向表尾方向訪問節點;用於遍歷操作。
    			struct zskiplistNode *forward;
    			// 跨度,用於計算兩個節點直接的距離;用於排序(排位)操作。
    			unsigned int span;
    		} level[];
    		// 後退指針,從表尾向表頭方向訪問節點
    		struct zskiplistNode *backward;
    		// 分值,注意節點按照此值從小到大排序
    		// 分值相同的節點,按照成員對象在字典中從小(越小越靠近表頭方向)到大進行排序
    		double score;
    		// 成員對象,指向字符串對象,而字符串對象保存 SDS 值。
    		// 同一個跳躍表,成員對象必須唯一,分值可以相同;
    		robj *obj;
    	} zskiplistNode;
    	// 跳躍表
    	typeof struct zskiplist {
    		// 指向跳躍表表頭節點,O(1)
    	    struct zskiplistNode * header;
    	    // 指向跳躍表表尾節點,O(1)
    	    struct zskiplistNode *tail;
    	    // 表中節點的數量,可以用於跳躍表長度計算。
    	    unsigned long length;
    	    // 表中層數最大的節點的層數,表頭層高不計算在內
    	    int level;
    	} zskiplist;
    
  3. 查找過程

    1. 從 header 的最高層開始遍歷找到第一個節點 (最後一個比「我」小的元素)
    2. 然後從這個節點開始降一層再遍歷找到第二個節點 (最後一個比「我」小的元素),
    3. 一直降到最底層進行遍歷就找到了期望的節點 (最底層的最後一個比我「小」的元素)。
    
  4. 插入操作

    	1. 找到要需要插入的位置的上一個節點 (時間複雜度: log(N))
    	2. 隨機生成一個層數, 層數符合冪次定律, 數值越大,生成的機率越小
    	3. 爲每層的新節點與新節點的前一個節點設置 span 值(到下一個節點的距離)
    	4. 如果分配的新節點的高度高於當前跳躍列表的最大高度,就需要更新一下跳躍列表的最大高度
    
  5. 刪除過程

    	1. 找到待刪除的節點x (時間複雜度 O(log(n)))
    	2. 修改x前一個節點每層的前進指針與span值 (O(1))
    	3. 修改後一個節點每層的後退指針 (O(1))
    	4. 修改跳錶長度 O(1)
    	5. 注意可能需要更新一下最高層數 maxLevel
    
  6. 更新過程 有兩種情況需要考慮

    1. 如果分數變化幅度較小,元素排名將沒有變化,只需要更改分數
      	判斷方式爲:使用當前更新節點的正向和向後指針訪問同級節點,判斷 score 是否仍在前一個節點和下一個節點之間的分數之間。
      
    2. 其他情況:先刪除,再插入。
  7. skiplist與平衡樹、哈希表的比較

    • 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 比平衡樹要簡單得多。

引用

redis zset內部實現
《Redis 設計與實現(第二版)》
《Redis 深度歷險:核心原理與應用實踐》

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章