SkipList 跳跃表
http://blog.csdn.net/likun_tech/article/details/7354306
http://www.cnblogs.com/zhuangli/articles/1275665.html
http://www.cnblogs.com/xuqiang/archive/2011/05/22/2053516.html
為什么選擇跳表
目前經常使用的平衡數據結構有:B樹,紅黑樹,AVL樹,Splay Tree, Treep等。
想象一下,給你一張草稿紙,一只筆,一個編輯器,你能立即實現一顆紅黑樹,或者AVL樹
出來嗎? 很難吧,這需要時間,要考慮很多細節,要參考一堆算法與數據結構之類的樹,
還要參考網上的代碼,相當麻煩。
用跳表吧,跳表是一種隨機化的數據結構,目前開源軟件 Redis 和 LevelDB 都有用到它,
它的效率和紅黑樹以及 AVL 樹不相上下,但跳表的原理相當簡單,只要你能熟練操作鏈表,
就能輕松實現一個 SkipList。
有序表的搜索
考慮一個有序表:
從該有序表中搜索元素 < 23, 43, 59 > ,需要比較的次數分別為 < 2, 4, 6 >,總共比較的次數
為 2 + 4 + 6 = 12 次。有沒有優化的算法嗎? ?鏈表是有序的,但不能使用二分查找。類似二叉
搜索樹,我們把一些節點提取出來,作為索引。得到如下結構:
這里我們把 < 14, 34, 50, 72 > 提取出來作為一級索引,這樣搜索的時候就可以減少比較次數了。
我們還可以再從一級索引提取一些元素出來,作為二級索引,變成如下結構:
?這里元素不多,體現不出優勢,如果元素足夠多,這種索引結構就能體現出優勢來了。
跳表
下面的結構是就是跳表:
其中 -1 表示 INT_MIN, 鏈表的最小值,1 表示 INT_MAX,鏈表的最大值。
跳表具有如下性質:
(1) 由很多層結構組成
(2) 每一層都是一個有序的鏈表
(3) 最底層(Level 1)的鏈表包含所有元素
(4) 如果一個元素出現在 Level i 的鏈表中,則它在 Level i 之下的鏈表也都會出現。
(5) 每個節點包含兩個指針,一個指向同一鏈表中的下一個元素,一個指向下面一層的元素。
跳表的搜索
例子:查找元素 117
(1) 比較 21, 比 21 大,往后面找
(2) 比較 37, ? 比 37大,比鏈表最大值小,從 37 的下面一層開始找
(3) 比較 71, ?比 71 大,比鏈表最大值小,從 71 的下面一層開始找
(4) 比較 85, 比 85 大,從后面找
(5) 比較 117, 等于 117, 找到了節點。
具體的搜索算法如下:
跳表的插入
先確定該元素要占據的層數 K(采用丟硬幣的方式,這完全是隨機的)
然后在 Level 1 ... Level K 各個層的鏈表都插入元素。
例子:插入 119, K = 2
如果 K 大于鏈表的層數,則要添加新的層。
例子:插入 119, K = 4
丟硬幣決定 K
插入元素的時候,元素所占有的層數完全是隨機的,通過一下隨機算法產生:
相當與做一次丟硬幣的實驗,如果遇到正面,繼續丟,遇到反面,則停止,
用實驗中丟硬幣的次數 K 作為元素占有的層數。顯然隨機變量 K 滿足參數為 p = 1/2 的幾何分布,
K 的期望值 E[K] = 1/p = 2. 就是說,各個元素的層數,期望值是 2 層。
跳表的高度。
n 個元素的跳表,每個元素插入的時候都要做一次實驗,用來決定元素占據的層數 K,
跳表的高度等于這 n 次實驗中產生的最大 K,待續。。。
跳表的空間復雜度分析
根據上面的分析,每個元素的期望高度為 2, 一個大小為 n 的跳表,其節點數目的
期望值是 2n。
跳表的刪除
在各個層中找到包含 x 的節點,使用標準的 delete from list 方法刪除該節點。
例子:刪除 71
http://www.cxphp.com/?p=234(Redis中c語言的實現)
http://imtinx.iteye.com/blog/1291165
http://kenby.iteye.com/blog/1187303
http://bbs.bccn.net/thread-228556-1-1.html
http://blog.csdn.net/xuqianghit/article/details/6948554(leveldb源碼)
二叉樹是我們都非常熟悉的一種數據結構。它支持包括查找、插入、刪除等一系列的操作。但它有一個致命的弱點,就是當數據的隨機性不夠時,會導致其樹型結構的不平衡,從而直接影響到算法的效率。
跳躍表(Skip List)是1987年才誕生的一種嶄新的數據結構,它在進行查找、插入、刪除等操作時的期望時間復雜度均為O(logn),有著近乎替代平衡樹的本領。而且最重要的一點,就是它的編程復雜度較同類的AVL樹,紅黑樹等要低得多,這使得其無論是在理解還是在推廣性上,都有著十分明顯的優勢。
跳躍表由多條鏈構成(S0,S1,S2 ……,Sh),且滿足如下三個條件:
(1)每條鏈必須包含兩個特殊元素:+∞ 和 -∞
(2)S0包含所有的元素,并且所有鏈中的元素按照升序排列。
(3)每條鏈中的元素集合必須包含于序數較小的鏈的元素集合,即:
【基本操作】
在對跳躍表有一個初步的認識以后,我們來看一下基于它的幾個最基本的操作。
一、查找
目的:在跳躍表中查找一個元素x
在跳躍表中查找一個元素x,按照如下幾個步驟進行:
i)從最上層的鏈(Sh)的開頭開始
ii)假設當前位置為p,它向右指向的節點為q(p與q不一定相鄰),且q的值為y。將y與x作比較
(1) x=y ? ? 輸出查詢成功及相關信息
(2) x>y ? ? 從p向右移動到q的位置
(3) x<y ? ? 從p向下移動一格
iii) ? ?如果當前位置在最底層的鏈中(S0),且還要往下移動的話,則輸出查詢失敗
二、插入
目的:向跳躍表中插入一個元素x
首先明確,向跳躍表中插入一個元素,相當于在表中插入一列從S0中某一位置出發向上的連續一段元素。有兩個參數需要確定,即插入列的位置以及它的“高度”。
關于插入的位置,我們先利用跳躍表的查找功能,找到比x小的最大的數y。根據跳躍表中所有鏈均是遞增序列的原則,x必然就插在y的后面。
而插入列的“高度”較前者來說顯得更加重要,也更加難以確定。由于它的不確定性,使得不同的決策可能會導致截然不同的算法效率。為了使插入數據之后,保持該數據結構進行各種操作均為O(logn)復雜度的性質,我們引入隨機化算法(Randomized Algorithms)。
我們定義一個隨機決策模塊,它的大致內容如下:
·產生一個0到1的隨機數r ? ? ? ? ? ? ? ? ? r ← random()
·如果r小于一個常數p,則執行方案A, ? ? ? if ?r<p then do A
否則,執行方案B ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? else do B
初始時列高為1。插入元素時,不停地執行隨機決策模塊。如果要求執行的是A操作,則將列的高度加1,并且繼續反復執行隨機決策模塊。直到第i次,模塊要求執行的是B操作,我們結束決策,并向跳躍表中插入一個高度為i的列。
性質1: ? ?根據上述決策方法,該列的高度大于等于k的概率為pk-1。
此處有一個地方需要注意,如果得到的i比當前跳躍表的高度h還要大的話,則需要增加新的鏈,使得跳躍表仍滿足先前所提到的條件。
我們來看一個例子:
假設當前我們要插入元素“40”,且在執行了隨機決策模塊后得到高度為4
·步驟一:找到表中比40小的最大的數,確定插入位置
·步驟二:插入高度為4的列,并維護跳躍表的結構
三、刪除
目的:從跳躍表中刪除一個元素x
刪除操作分為以下三個步驟:
(1)在跳躍表中查找到這個元素的位置,如果未找到,則退出 ? ? *
(2)將該元素所在整列從表中刪除 ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?*
(3)將多余的“空鏈”刪除 ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?*
所謂“記憶化”查找,就是在前一次查找的基礎上進行進一步的查找。它可以利用前一次查找所得到的信息,取其中可以被當前查找所利用的部分。利用“記憶化”查找可以將一次查找的復雜度變為O(logk),其中k為此次與前一次兩個被查找元素在跳躍表中位置的距離。
下面來看一下記憶化搜索的具體實現方法:
假設上一次操作我們查詢的元素為i,此次操作我們欲查詢的元素為j。我們用一個update數組來記錄在查找i時,指針在每一層所“跳”到的最右邊的位置。如圖4.1中橘×××的元素。(藍色為路徑上的其它元素)
在插入元素j時,分為兩種情況:
(1)i<=j
從S0層開始向上遍歷update數組中的元素,直到找到某個元素,它向右指向的元素大于等于j,并于此處開始新一輪對j的查找(與一般的查找過程相同)
(2)i>j
從S0層開始向上遍歷update數組中的元素,直到找到某個元素小于等于j,并于此處開始新一輪對j的查找(與一般的查找過程相同)
圖4.2十分詳細地說明了在查找了i=37之后,繼續查找j=15或53時的兩種不同情況。
【復雜度分析】
一個數據結構的好壞大部分取決于它自身的空間復雜度以及基于它一系列操作的時間復雜度。跳躍表之所以被譽為幾乎能夠代替平衡樹,其復雜度方面自然不會落后。我們來看一下跳躍表的相關復雜度:
空間復雜度: O(n) ? ? ? ? ? (期望)
跳躍表高度: O(logn) ? ? ? ?(期望)
相關操作的時間復雜度:
查找: O(logn) ? ? ? ?(期望)
插入: ?O(logn) ? ? ? ?(期望)
刪除: O(logn) ? ? ? ?(期望)
之所以在每一項后面都加一個“期望”,是因為跳躍表的復雜度分析是基于概率論的。有可能會產生最壞情況,不過這種概率極其微小。
下面我們來一項一項分析。
一、 空間復雜度分析 O(n)
假設一共有n個元素。根據性質1,每個元素插入到第i層(Si)的概率為pi-1 ,則在第i層插入的期望元素個數為npi-1,跳躍表的元素期望個數為 ,當p取小于0.5的數時,次數總和小于2n。
所以總的空間復雜度為O(n)
二、跳躍表高度分析 O(logn)
根據性質1,每個元素插入到第i層(Si)的概率為pi ,則在第i層插入的期望元素個數為npi-1。
考慮一個特殊的層:第1+ 層。
層的元素期望個數為 ?= 1/n2,當n取較大數時,這個式子的值接近0,故跳躍表的高度為O(logn)級別的。
三、查找的時間復雜度分析 O(logn)
我們采用逆向分析的方法。假設我們現在在目標節點,想要走到跳躍表最左上方的開始節點。這條路徑的長度,即可理解為查找的時間復雜度。
設當前在第i層第j列那個節點上。
i)如果第j列恰好只有i層(對應插入這個元素時第i次調用隨機化模塊時所產生的B決策,概率為1-p),則當前這個位置必然是從左方的某個節點向右跳過來的。
ii)如果第j列的層數大于i(對應插入這個元素時第i次調用隨機化模塊時所產生的A決策,概率為p),則當前這個位置必然是從上方跳下來的。(不可能從左方來,否則在以前就已經跳到當前節點上方的節點了,不會跳到當前節點左方的節點)
設C(k)為向上跳k層的期望步數(包括橫向跳躍)
有:
C(0) = 0
C(k) = (1-p)(1+向左跳躍之后的步數)+p(1+向上跳躍之后的步數)
? ? = (1-p)(1+C(k)) + p(1+C(k-1))
C(k) = 1/p + C(k-1)
C(k) = k/p
而跳躍表的高度又是logn級別的,故查找的復雜度也為logn級別。
對于記憶化查找(Search with fingers)技術我們可以采用類似的方法分析,很容易得出它的復雜度是O(logk)的(其中k為此次與前一次兩個被查找元素在跳躍表中位置的距離)。
四、插入與刪除的時間復雜度分析 O(logn)
插入和刪除都由查找和更新兩部分構成。查找的時間復雜度為O(logn),更新部分的復雜度又與跳躍表的高度成正比,即也為O(logn)。
所以,插入和刪除操作的時間復雜度都為O(logn)
五、實際測試效果
(1)不同的p對算法復雜度的影響
P | 平均操作時間 | 平均列高 | 總結點數 | 每次查找跳躍次數 (平均值) | 每次插入跳躍次數 (平均值) | 每次刪除跳躍次數 (平均值) |
2/3 | 0.0024690 ms | 3.004 | 91233 | 39.878 | 41.604 | 41.566 |
1/2 | 0.0020180 ms | 1.995 | 60683 | 27.807 | 29.947 | 29.072 |
1/e | 0.0019870 ms | 1.584 | 47570 | 27.332 | 28.238 | 28.452 |
1/4 | 0.0021720 ms | 1.330 | 40478 | 28.726 | 29.472 | 29.664 |
1/8 | 0.0026880 ms | 1.144 | 34420 | 35.147 | 35.821 | 36.007 |
表1 ? 進行106次隨機操作后的統計結果
從表1中可見,當p取1/2和1/e的時候,時間效率比較高(為什么?)。而如果在實際應用中空間要求很嚴格的話,那就可以考慮取稍小一些的p,如1/4。
(2)運用“記憶化”查找 (Search with fingers) 的效果分析
所謂“記憶化”查找,就是在前一次查找的基礎上進行進一步的查找。它可以利用前一次查找所得到的信息,取其中可以被當前查找所利用的部分。利用“記憶化”查找可以將一次查找的復雜度變為O(logk),其中k為此次與前一次兩個被查找元素在跳躍表中位置的距離。
P | 數據類型 | 平均操作時間(不運用記憶化查找) | 平均操作時間(運用記憶化查找) | 平均每次查找跳躍次數(不運用記憶化查找) | 平均每次查找跳躍次數(運用記憶化查找) |
0.5 | 隨機(相鄰被查找元素鍵值差的絕對值較大) | 0.0020150 ms | 0.0020790 ms | 23.262 | 26.509 |
0.5 | 前后具備相關性(相鄰被查找元素鍵值差的絕對值較小) | 0.0008440 ms | 0.0006880 ms | 26.157 | 4.932 |
表1 ? 進行106次相關操作后的統計結果
從表2中可見,當數據相鄰被查找元素鍵值差絕對值較小的時候,我們運用“記憶化”查找的優勢是很明顯的,不過當數據隨機化程度比較高的時候,“記憶化”查找不但不能提高效率,反而會因為跳躍次數過多而成為算法的瓶頸。
合理地利用此項優化,可以在特定的情況下將算法效率提升一個層次。
【跳躍表的應用】
高效率的相關操作和較低的編程復雜度使得跳躍表在實際應用中的范圍十分廣泛。尤其在那些編程時間特別緊張的情況下,高性價比的跳躍表很可能會成為你的得力助手。
能運用到跳躍表的地方很多,與其去翻陳年老題,不如來個趁熱打鐵,拿NOI2004第一試的第一題——郁悶的出納員(Cashier)來“小試牛刀”吧。
例題一:NOI2004 Day1 郁悶的出納員(Cashier)
[點擊查看附錄中的原題]
這道題解法的多樣性給了我們一次對比的機會。用不同的算法和數據結構,在效率上會有怎樣的差異呢?
首先定義幾個變量
? ?R – 工資的范圍
? ?N – 員工總數
我們來看一下每一種適用的算法和數據結構的簡要描述和理論復雜度:
(1)線段樹
簡要描述:以工資為關鍵字構造線段樹,并完成相關操作。
I命令時間復雜度:O(logR)
A命令時間復雜度:O(1)
S命令時間復雜度:O(logR)
F命令時間復雜度:O(logR)
(2)伸展樹(Splay tree)
簡要描述:以工資為關鍵字構造伸展樹,并通過“旋轉”完成相關操作。
I命令時間復雜度:O(logN)
A命令時間復雜度:O(1)
S命令時間復雜度:O(logN)
F命令時間復雜度:O(logN)
(3)跳躍表(Skip List)
簡要描述:運用跳躍表數據結構完成相關操作。
I命令時間復雜度:O(logN)
A命令時間復雜度:O(1)
S命令時間復雜度:O(logN)
F命令時間復雜度:O(logN)
實際效果評測: (單位:秒)
Test1 | Test2 | Test3 | Test4 | Test5 | Test6 | Test7 | Test8 | Test9 | Test10 | |
線段樹 | 0.000 | 0.000 | 0.000 | 0.031 | 0.062 | 0.094 | 0.109 | 0.203 | 0.265 | 0.250 |
伸展樹 | 0.000 | 0.000 | 0.016 | 0.062 | 0.047 | 0.125 | 0.141 | 0.360 | 0.453 | 0.422 |
跳躍表 | 0.000 | 0.000 | 0.000 | 0.047 | 0.062 | 0.109 | 0.156 | 0.368 | 0.438 | 0.375 |
從結果來看,線段樹這種經典的數據結構似乎占據著很大的優勢。可有一點萬萬不能忽略,那就是線段樹是基于鍵值構造的,它受到鍵值范圍的約束。在本題中R的范圍只有105級別,這在內存較寬裕的情況下還是可以接受的。但是如果問題要求的鍵值范圍較大,或者根本就不是整數時,線段樹可就很難適應了。這時候我們就不得不考慮伸展樹、跳躍表這類基于元素構造的數據結構。而從實際測試結果看,跳躍表的效率并不比伸展樹差。加上編程復雜度上的優勢,跳躍表盡顯出其簡單高效的特點。
轉載于:https://blog.51cto.com/8701404/1386866
《新程序員》:云原生和全面數字化實踐50位技術專家共同創作,文字、視頻、音頻交互閱讀總結
以上是生活随笔為你收集整理的SkipList 跳跃表的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: MapReduce英语面试
- 下一篇: iOS-应用之间调用