維基百科:跳錶是一種數據結構。它使得包含n個元素的有序序列的查找和插入操做的平均時間複雜度都是 O(logn),優於數組的 O(n)複雜度。快速的查詢效果是經過維護一個多層次的鏈表實現的,且與前一層(下面一層)鏈表元素的數量相比,每一層鏈表中的元素的數量更少。前端
- 優於數組的插入操做時間複雜度
簡單理解跳錶是基於鏈表實現的有序列表,跳錶經過維護一個多層級的鏈表實現了快速查詢效果將平均時間複雜度降到了O($log^n$),這是一個典型的異空間換時間數據結構。git
在實際開發中常常遇到須要在數據集中查找一個指定數據的場景,而經常使用的支持高效查找算法的實現方式有如下幾種:算法
有序數組。插入時能夠先對數據排序,查詢時能夠採用二分查找算法下降查找操做的複雜度。缺點是插入和刪除數據時,爲了保持元素的有序性,須要進行大量數據的移動操做。數組
二叉查找樹。既支持高效的二分查找算法,又能快速的進行插入和刪除操做的數據結構,理想的時間複雜度爲 O($log^n$),可是在某些極端狀況下,二叉查找樹有可能變成一個線性鏈表,即退化成鏈表結構。數據結構
平衡二叉樹。基於二叉查找樹的優勢,對其缺點進行了優化改進,引入了平衡的概念。爲了維持二叉樹的平衡衍生出了多種平衡算法,根據平衡算法的不一樣具體實現有AVL樹 /B樹(B-Tree)/ B+樹(B+Tree)/紅黑樹 等等。可是平衡算法的實現大多數比較複雜且較難理解。優化
針對大致量、海量數據集中查找指定數據有更好的解決方案,咱們得評估時間、空間的成本和收益。3d
跳錶一樣支持對數據進行高效的查找,插入和刪除數據操做時間複雜度能與平衡二叉樹媲美,最重要的是跳錶的實現比平衡二叉樹簡單幾個級別。缺點就是「以空間換時間」方式存在必定數據冗餘。指針
若是存儲的數據是大對象,跳錶冗餘的只是指向數據的指針,幾乎能夠不計使用的內存空間。code
添加、刪除操做都須要先查詢出操做數據的位置,因此理解了跳錶的查詢原理後,剩下的只是對鏈表的操做。對象
例設原始鏈表上的有序數據爲【9,11,14,19,20,24,27】,若是我要查找的數據是20,只能從頭結點沿着鏈表依次比較查找,如圖所示:
鏈表不能像數組那樣經過索引快速訪問數據,只能沿着指針方向依次訪問,因此不能使用二分查找算法快速進行數據查詢。可是能夠借鑑建立索引的這種思路,就像圖書的目錄同樣,若是我要查看第六章的內容,直接翻到經過目錄查詢到的第六章對應頁碼處就行。
這裏的目錄就至關於建立的索引,該索引可以縮小咱們查詢數據的範圍減小查詢次數。在原始鏈表的基礎上,咱們增長一層索引鏈表,假如原始鏈表的每兩個結點就有一個結點也在索引鏈表當中,如圖所示:
當創建了索引後檢索數據的方式就發生了變化,當咱們想要定位到DataNode-20
,咱們不須要在原始鏈表中一個一個結點訪問,而是首先訪問索引鏈表:
因爲索引鏈表的結點個數是原始鏈表的一半,查找結點所需的訪問次數也就相應減小了一半,通過兩次查詢咱們便找到DataNode-20
。
正如圖書的目錄不止按照「章節」劃分,還能夠按照「第幾部分」、「第幾小節」進行劃分,鏈表的索引也同樣。咱們能夠繼續爲鏈表建立更多層索引,每層索引節點爲前一層索引(對應圖例的下一層)的一半,在數據量比較大時可以大大的提高咱們的查詢效率。
如圖所示,咱們基於原始鏈表的第1層索引,抽出了第2層更爲稀疏的索引,結點數量是第1層索引的一半。這樣的多層索引能夠進一步提高查詢效率,那麼它是如何進行查詢的呢?假如此次要查找DataNode-27
,讓咱們來演示一下檢索過程:
HeadIndex-7
開始查找,HeadIndex-7
指向DataNode-7
比DataNode-27
小,因此繼續向右查詢找到第二個索引節點IndexNode-20
。IndexNode-20
指向DataNode-20
也比DataNode-27
小,可是此時第二層已經沒有後續的索引節點,因此咱們須要順着IndexNode-20
訪問下一層索引,即第一層的IndexNode-20
。從索引節點訪問方式可知,索引節點保存着「數據節點」、「下層索引節點」的指針。
IndexNode-20
繼續向右檢索找到IndexNode-27
便檢索到了DataNode-27
。總結:
維基百科:
跳躍列表是按層建造的。底層是一個普通的有序鏈表。每一個更高層都充當下面列表的「快速通道」,這裏在第 i 層中的元素按某個固定的機率 $p$(一般爲 $\frac 12$ 或 $\frac 14$ 出如今第 i + 1 層中。每一個元素平均出如今 ${1\over 1-p}$ 個列表中,而最高層的元素(一般是在跳躍列表前端的一個特殊的頭元素)在 $log_{1/p}^n$個列表中出現。在查找目標元素時,從頂層列表、頭元素起步。算法沿着每層鏈表搜索,直至找到一個大於或等於目標的元素,或者到達當前層列表末尾。若是該元素等於目標元素,則代表該元素已被找到;若是該元素大於目標元素或已到達鏈表末尾,則退回到當前層的上一個元素,而後轉入下一層進行搜索。每層鏈表中預期的查找步數最多爲$\frac 1p$,而層數爲 -${log_p^n}\over{p}$,因爲 $p$ 是常數,查找操做整體的時間複雜度爲 O($log^n$)。而經過選擇不一樣 $p$ 值,就能夠在查找代價和存儲代價之間獲取平衡。
上面的查詢例子中索引節點已是建立好的,那麼原始鏈表哪些數據節點須要建立索引節點、何時建立?這些問題的答案都要回歸到往原始鏈表添加數據時。
從上面的總結不難理解在向原始鏈表中插入數據時,當前插入的數據按照某個固定的機率$p$($\frac 12$ 或 $\frac 14$)在每層索引鏈表中建立索引節點。假設如今插入DataNode-18
,咱們來看看是如何插入和建立索引節點的:
首先咱們按照跳錶查找結點的方法,找到待插入結點的前置結點(僅小於待插入結點):
接下來按照通常鏈表的插入方式,把DataNode-18
插入到結點DataNode-14
的後續位置:
這樣數據就插入到了原始鏈表中,可是咱們的插入操做並無結束。按照定義咱們須要讓新插入的結點隨機(拋硬幣的方式)「晉升」,也就是爲DataNode-18
建立索引節點,正是採起這種簡單的隨機方式,跳錶也被稱爲一種隨機化的數據結構。
假設第1、第二次隨機的結果都是晉升成功,那麼咱們須要爲DataNode-18
建立索引節點,插入到第一層和第二層索引的對應位置,而且向下指向原始鏈表的DataNode-18
。
在索引鏈表中插入新建立的索引節點時須要注意幾點:
- 找到待插入索引節點的前置索引節點指向新索引節點,新索引節點指向前置節點以前指向的索引節點。(也就是鏈表的插入操做)
- 隨機的結果是「晉升成功」就能夠繼續向上一層建立索引,直到假設隨機的結果是「晉升失敗」或者「新增索引層」。
- 每層是否建立索引節點能夠一次性拋幾回硬幣,而不是添加一層索引後再進行投幣。(這樣作的目的是爲了更好的用代碼實現)。
新建的索引節點何如銜接到前置索引節點以及如何用代碼實現,這個咱們在下篇文章「SkipList 代碼實現」去解析。
若是在第二層(目前索引最大層級)建立索引節點後,下一次隨機的結果仍然是晉升成功,這時候該怎麼辦呢?這個時候咱們就須要添加一層索引層:
能夠看到此時第三層只有HeadIndex-7
和IndexNode-18
,此時不會繼續向上層建立索引,由於就算繼續建立仍只有HeadIndex-7
和IndexNode-18
,這顯得毫無心義。至此跳錶的插入操做包括索引的建立過程已經解析完,跳錶的刪除過程正好和插入是相反的思路。
假設咱們要刪除剛纔插入的DataNode-18
,首先咱們要按照跳錶查找結點的方法找到待刪除的DataNode-18
,固然若是沒有找到對應的數據直接返回進行。
接下來按照鏈表的刪除方式,把DataNode-18
從原始鏈表當中刪除
同插入數據同樣,刪除工做並無就此完成,咱們須要將DataNode-18
在索引層對應的IndexNode-18
也一 一刪除:
同插入索引節點同樣,刪除索引節點時也須要維護前置節點的指向關係。這裏須要特別注意最上層索引(第三層),當刪除IndexNode-18
後該層只剩下HeadIndex-7
,這個時候須要將該索引層也一同刪除。
至此整個刪除操做就算完成了,此時跳錶的結構就和咱們以前插入以前保持一致了:
總結
- 簡單對比了跳錶和其餘幾種高效查找算法的優缺點。
- 跳錶是基於鏈表實現的,是一種「以空間換時間」的「隨機化」數據結構。
- 跳錶引入了索引層的概念,有了它纔有了時間複雜度爲O($logn$)的查詢效率,從而實現了增刪操做的時間複雜度也是O($logn$)。
- 跳錶擁有平衡二叉樹相同的查詢效率,可是跳錶對於樹平衡的實現是基於一種隨機化的算法的,相對於AVL樹/B樹(B-Tree)/B+樹(B+Tree)/紅黑樹的實現簡單得多。
可恥的貼個我的Git地址:SkipList原理篇