本篇博客主要介紹一下跳躍表的原理和簡單實現。
什麼是跳躍表?
增加了向前指針的鏈表叫做跳錶,跳錶全稱跳躍表,簡稱跳錶。
- 跳錶是一個隨機化的數據結構,實質就是一種可以進行二分查找的有序鏈表;
- 跳錶在原有的有序鏈表上面增加了多級索引,通過索引實現快速查找;
- 跳錶不僅能提高搜索性能,同時也可以提供插入和刪除操作的性能。
跳躍表的原理
我們知道,在一個單鏈表中如果想要根據值來查找一個元素,時間複雜度爲,即使該鏈表是有序的,也不能通過二分的方式來降低時間複雜度。
如上圖,如果我們要查找值爲52的元素的結點,我們必須從頭結點開始,循環遍歷知道找到值爲52的結點,也就是最後一個結點,一共比較8次(-INF,負無窮不算)。
我們有沒有什麼辦法來減少訪問的次數呢?我們可以試着去開闢一條捷徑去訪問52。
如上圖,我們要查找值爲52的結點,只需要在L2層查找4次即可找到。
在這個結構中,查詢值爲47的結點只需要查詢5次。先在L2層查找47,查詢4次後找到52,因爲鏈表是有序的,所以我們回退到39,然後去L1層去查找,查詢一次就查到了47,所以一共需要查找5次。
我們能不能讓查找值爲52結點的次數再減少呢?我們可以考慮再開闢一條捷徑。
如上圖,我們查找值爲52的結點只需要在L3層查找2次即可。
查找元素47仍然是最耗時的,需要查詢5次。先在L3層查詢2次,再在L2層查詢2次,最後到L1層查詢一次。
從上面的查找過程看,這種思想和二分的思想非常相似,我們再將結構完善一下,最終結構如下:
我們可以看出最耗時的訪問仍然是訪問值爲47的結點,一共需要查詢6次。L4層訪問52,L3訪問25、52,L2訪問39、52,L1訪問47,共6次。
跳躍表的時間複雜度
如果有n個元素,因爲是二分,所以層數就應該是層,再加上自身的1層。
- 以上圖爲例,如果是4個元素,那麼分層爲L4和L3,再加上本身的L2,一共3層;如果是8個元素,那麼就是3 + 1層。
最耗時間的查詢自然是訪問所有層數,耗時,即。爲什麼是呢?
- 我們以訪問47來分析一下,查詢到47要訪問所有的分層,每個分層都要訪問2個元素,中間元素和最後一個元素。
所以跳躍表的時間複雜度爲。
跳躍表的實現分析
從上面跳躍表的時間複雜度的分析我們可以看出,該跳躍表是比較理想的。
但是如果想要在跳躍表中插入或者刪除一個元素呢?比如我們插入一個元素22、23、24…,自然是在L1層插入,但是這些元素在L1層插入之後,那麼如何調整L2層和L3層的連接來維持這個比較理想的跳躍表呢?
我們知道,平衡二叉搜索樹的調整是一件令人非常頭疼的事,左旋、右旋、左右旋;而調整一個理想的跳躍表將是一個比調整平衡二叉搜索樹還複雜的操作。
但是這裏,我們不需要通過複雜的操作調整連接來維護這樣完美的跳躍表。有一種基於概率統計的插入算法,也能得到時間複雜度爲的查詢效率,這種跳躍表是比較實用的。
跳躍表的插入分析
從理想跳躍表,我們可以看出,L2層元素的數量是L1層元素數量的,L3層元素數量是L2層元素數量的,依次類推。從這裏,我們可以想到,只要在插入時儘量保證上一層的元素個數是下一層元素的,我們的跳躍表就能成爲理想的跳躍表。
那麼如何在插入時保證上一層元素的個數是下一層元素個數的呢?我們可以通過拋硬幣的方式來決定。
- 假設元素e要插入跳躍表,很顯然,L1層肯定要插入元素e。
- 那麼L2層要不要插入e呢?我們希望上層元素個數是下層元素個數的,所以我們有的概率希望e插入L2層,那麼拋一下硬幣吧,正面朝上就插入,反面就不插入。
- 那麼L3到底要不要插入e呢?相對於L2層,我們還是希望的概率插入,可以繼續拋硬幣。
- 依次類推,元素e插入第i層的概率就是。這樣,我們就能在跳躍表中插入一個元素了。
- 如果插入元素的數量較小,結果很可能不是一個理想的跳躍表。但是如果元素數量非常大,根據概率學我們可以知道,最終的表結構肯定非常接近於理想的跳躍表。
插入操作依賴於查詢操作,所以插入操作的時間複雜度同樣爲。
跳躍表的刪除分析
刪除操作和普通的鏈表刪除操作完全一樣,直接刪除元素,然後調整一下刪除元素後的指針就可以了。
刪除操作同樣依賴於查詢操作,所以刪除操作的時間複雜度同樣也是。
代碼實現
代碼實現可參考: