資料結構 | SkipList(跳錶)
寫在前面
該文並不是跳錶的入門文章,而是致力於以簡潔精煉的語言來描述 SkipList,來彌補上次面試時被問到跳錶結果腦中只有圖片沒有文字的尷尬場景。。。
SkipList(跳錶)
SkipList 是一種查詢結構
結構
它的結構是一個有序連結串列,但是該連結串列的節點的具有多個指標,且不止指向下一個節點。在該連結串列上,每 N 個節點還會擁有一個指向後第 N 個節點的指標,N 值通常為 2 的冪。一個節點上可能具有多個 N 值指標,指標按照 N 值大小從高到低排序,分為多個"層"
查詢
當要查詢的時候:
- 頭節點具有所有的 N 值的指標,頭節點為當前節點
- 取出當前節點具有的最高的 N 值的指標
- 觀察該指標指向的節點的值
- 如果等於目標值,說明節點找到,退出
- 如果大於目標值,將當前高度減一
- 若此時高度為零,說明值不存在,退出
- 否則,返回到 第 3 步
- 如果小於目標值,前進到該節點,返回到 第 2 步
- 當來到尾部時,代表值不存在,退出
在查詢的時候,由於 N 值為 2 的冪,故每一次 高度減一 ,需要查詢的節點就會減少一半,所以時間複雜度為 \(O(logN)\)
但如果這樣,由於插入和刪除時需要維護 N 值的正確性,時間複雜度可能退化為 \(O(N)\) 。所以改進為了插入時高度隨機取值。
且為了保證層數越高越稀疏,該隨機值不會隨機分佈,其計算過程如下:
- 起始高度為 1
- 下一層被建立的概率為 \(P\)
- 若未到達高度上限,且建立成功,則回到第二步
- 此時的高度值則為要插入節點的當前高度
插入
對於插入來講,由於我們使用了隨機高度的做法,所以不需要維護 N 值的正確性,故變得十分簡單。
只需要先進行一次查詢的過程,同時記錄發生了高度下降的節點,在查詢完成後根據高度,來從記錄中更改引用關係。
時間複雜度為 \(O(logN)\)
刪除
刪除則也與插入同理,只不過更改引用關係時執行的是將前置節點與後置節點連線的操作。
但是需要這需要對所有高度進行查詢,所以節點最好選用雙向連結串列,可以減少複雜度。
時間複雜度為 \(O(logN)\)
與紅黑樹的對比
跳錶的時間複雜度與紅黑樹相同,且都是用於在記憶體中的一種查詢結構。
當然也有不同的地方,Redis 的開發者還給出了使用跳錶的理由
There are a few reasons:
They are not very memory intensive. It's up to you basically. Changing parameters about the probability of a node to have a given number of levels will make then less memory intensive than btrees.
A sorted set is often target of many ZRANGE or ZREVRANGE operations, that is, traversing the skip list as a linked list. With this operation the cache locality of skip lists is at least as good as with other kind of balanced trees.
They are simpler to implement, debug, and so forth. For instance thanks to the skip list simplicity I received a patch (already in Redis master) with augmented skip lists implementing ZRANK in O(log(N)). It required little changes to the code.