跳躍表

一、相關面試題:

1.跳躍表有沒有聽說過?

2.如何讓鏈表的元素查詢接近線性時間?


二、跳躍表的引入

我們知道,普通單鏈表查詢一個元素的時間複雜度爲O(n),即使該單鏈表是有序的,我們也不能通過二分的方式縮減時間複雜度。

  

如上圖,我們要查詢元素爲55的結點,必須從頭結點,循環遍歷到最後一個節點,不算-INF(負無窮)一共查詢8次。那麼用什麼辦法能夠用更少的次數訪問55呢?最直觀的,當然是新開闢一條捷徑去訪問55。

 

如上圖,我們要查詢元素爲55的結點,只需要在L2層查找4次即可。在這個結構中,查詢結點爲46的元素將耗費最多的查詢次數5次。即先在L2查詢46,查詢4次後找到元素55,因爲鏈表是有序的,46一定在55的左邊,所以L2層沒有元素46。然後我們退回到元素37,到它的下一層即L1層繼續搜索46。非常幸運,我們只需要再查詢1次就能找到46。這樣一共耗費5次查詢。

那麼,如何才能更快的搜尋55呢?有了上面的經驗,我們就很容易想到,再開闢一條捷徑。

如上圖,我們搜索55只需要2次查找即可。這個結構中,查詢元素46仍然是最耗時的,需要查詢5次。即首先在L3層查找2次,然後在L2層查找2次,最後在L1層查找1次,共5次。很顯然,這種思想和2分非常相似,那麼我們最後的結構圖就應該如下圖。 

我們可以看到,最耗時的訪問46需要6次查詢。即L4訪問55,L3訪問21、55,L2訪問37、55,L1訪問46。我們直覺上認爲,這樣的結構會讓查詢有序鏈表的某個元素更快。那麼究竟算法複雜度是多少呢?

 

如上圖,我們要查詢元素爲55的結點,只需要在L2層查找4次即可。在這個結構中,查詢結點爲46的元素將耗費最多的查詢次數5次。即先在L2查詢46,查詢4次後找到元素55,因爲鏈表是有序的,46一定在55的左邊,所以L2層沒有元素46。然後我們退回到元素37,到它的下一層即L1層繼續搜索46。非常幸運,我們只需要再查詢1次就能找到46。這樣一共耗費5次查詢。

那麼,如何才能更快的搜尋55呢?有了上面的經驗,我們就很容易想到,再開闢一條捷徑。

如上圖,我們搜索55只需要2次查找即可。這個結構中,查詢元素46仍然是最耗時的,需要查詢5次。即首先在L3層查找2次,然後在L2層查找2次,最後在L1層查找1次,共5次。很顯然,這種思想和2分非常相似,那麼我們最後的結構圖就應該如下圖。 

我們可以看到,最耗時的訪問46需要6次查詢。即L4訪問55,L3訪問21、55,L2訪問37、55,L1訪問46。我們直覺上認爲,這樣的結構會讓查詢有序鏈表的某個元素更快。那麼究竟算法複雜度是多少呢?

如果有n個元素,因爲是2分,所以層數就應該是log n層 (本文所有log都是以2爲底),再加上自身的1層。以上圖爲例,如果是4個元素,那麼分層爲L3和L4,再加上本身的L2,一共3層;如果是8個元素,那麼就是3+1層。最耗時間的查詢自然是訪問所有層數,耗時logn+logn,即2logn。爲什麼是2倍的logn呢?我們以上圖中的46爲例,查詢到46要訪問所有的分層,每個分層都要訪問2個元素,中間元素和最後一個元素。所以時間複雜度爲O(logn)

 

至此爲止,我們引入了最理想的跳躍表,但是如果想要在上圖中插入或者刪除一個元素呢?比如我們要插入一個元素22、23、24……,自然在L1層,我們將這些元素插入在元素21後,那麼L2層,L3層呢?我們是不是要考慮插入後怎樣調整連接,才能維持這個理想的跳躍表結構。我們知道,平衡二叉樹的調整是一件令人頭痛的事情,左旋右旋左右旋……一般人還真記不住,而調整一個理想的跳躍表將是一個比調整平衡二叉樹還複雜的操作。幸運的是,我們並不需要通過複雜的操作調整連接來維護這樣完美的跳躍表。有一種基於概率統計的插入算法,也能得到時間複雜度爲O(logn)的查詢效率,這種跳躍表纔是我們真正要實現的。


三、跳躍表的實現

容易實現的跳躍表,它允許簡單的插入和刪除元素,並提供O(logn)的查詢時間複雜度,以下我們簡稱爲跳躍表。

先討論插入,我們先看理想的跳躍表結構,L2層的元素個數是L1層元素個數的1/2,L3層的元素個數是L2層的元素個數的1/2,以此類推。從這裏,我們可以想到,只要在插入時儘量保證上一層的元素個數是下一層元素的1/2,我們的跳躍表就能成爲理想的跳躍表。那麼怎麼樣才能在插入時保證上一層元素個數是下一層元素個數的1/2呢?很簡單,拋硬幣就能解決了!假設元素X要插入跳躍表,很顯然,L1層肯定要插入X。那麼L2層要不要插入X呢?我們希望上層元素個數是下層元素個數的1/2,所以我們有1/2的概率希望X插入L2層,那麼拋一下硬幣吧,正面就插入,反面就不插入。那麼L3到底要不要插入X呢?相對於L2層,我們還是希望1/2的概率插入,那麼繼續拋硬幣吧!以此類推,元素X插入第n層的概率是(1/2)的n次。這樣,我們能在跳躍表中插入一個元素了。

在此還是以上圖爲例:跳躍表的初試狀態如下圖,表中沒有一個元素:

 

如果我們要插入元素2,首先是在底部插入元素2,如下圖:

 

然後我們拋硬幣,結果是正面,那麼我們要將2插入到L2層,如下圖

 

繼續拋硬幣,結果是反面,那麼元素2的插入操作就停止了,插入後的表結構就是上圖所示。接下來,我們插入元素33,跟元素2的插入一樣,現在L1層插入33,如下圖:

 

然後拋硬幣,結果是反面,那麼元素33的插入操作就結束了,插入後的表結構就是上圖所示。接下來,我們插入元素55,首先在L1插入55,插入後如下圖:

 

然後拋硬幣,結果是正面,那麼L2層需要插入55,如下圖:

  

繼續拋硬幣,結果又是正面,那麼L3層需要插入55,如下圖:

  

繼續拋硬幣,結果又是正面,那麼要在L4插入55,結果如下圖:

  

繼續拋硬幣,結果是反面,那麼55的插入結束,表結構就如上圖所示。

以此類推,我們插入剩餘的元素。當然因爲規模小,結果很可能不是一個理想的跳躍表。但是如果元素個數n的規模很大,學過概率論的同學都知道,最終的表結構肯定非常接近於理想跳躍表。

當然,這樣的分析在感性上是很直接的,但是時間複雜度的證明實在複雜,在此我就不深究了,感興趣的可以去看關於跳躍表的paper。

再討論刪除,刪除操作沒什麼講的,直接刪除元素,然後調整一下刪除元素後的指針即可。跟普通的鏈表刪除操作完全一樣。

再來討論一下時間複雜度,插入和刪除的時間複雜度就是查詢元素插入位置的時間複雜度,這不難理解,所以是O(logn)。

發佈了216 篇原創文章 · 獲贊 54 · 訪問量 17萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章