skiplist
-
skiplist 是有序集合底層實現之一,另外一個是 ziplist。同時滿足以下條件時使用 ziplist
1. 元素數量小於 128 個,可以通過 zset-max-ziplist-entries 來修改 2. 所有 member 的長度都小於 64 字節,可以通過 zset-max-ziplist-value 來修改
skiplist 插入、刪除、查找的複雜度均爲O(logN)
-
結構
// 跳躍表節點 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;
-
查找過程
1. 從 header 的最高層開始遍歷找到第一個節點 (最後一個比「我」小的元素) 2. 然後從這個節點開始降一層再遍歷找到第二個節點 (最後一個比「我」小的元素), 3. 一直降到最底層進行遍歷就找到了期望的節點 (最底層的最後一個比我「小」的元素)。
-
插入操作
1. 找到要需要插入的位置的上一個節點 (時間複雜度: log(N)) 2. 隨機生成一個層數, 層數符合冪次定律, 數值越大,生成的機率越小 3. 爲每層的新節點與新節點的前一個節點設置 span 值(到下一個節點的距離) 4. 如果分配的新節點的高度高於當前跳躍列表的最大高度,就需要更新一下跳躍列表的最大高度
-
刪除過程
1. 找到待刪除的節點x (時間複雜度 O(log(n))) 2. 修改x前一個節點每層的前進指針與span值 (O(1)) 3. 修改後一個節點每層的後退指針 (O(1)) 4. 修改跳錶長度 O(1) 5. 注意可能需要更新一下最高層數 maxLevel
-
更新過程 有兩種情況需要考慮
- 如果分數變化幅度較小,元素排名將沒有變化,只需要更改分數
判斷方式爲:使用當前更新節點的正向和向後指針訪問同級節點,判斷 score 是否仍在前一個節點和下一個節點之間的分數之間。
- 其他情況:先刪除,再插入。
- 如果分數變化幅度較小,元素排名將沒有變化,只需要更改分數
-
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 比平衡樹要簡單得多。