本文收錄於專輯:http://dwz.win/HjK,點擊解鎖更多數據結構與算法的知識。java
你好,我是彤哥。面試
上一節,咱們一塊兒學習了關於哈希的一切,特別是哈希表的進化過程,相信經過上一節的學習,你必定能夠從頭至尾完整地給面試官講講哈希表是如何發展到現在這一步的。算法
可是,難道HashMap的終極形態只能經過「數組+鏈表+紅黑樹」的形式實現嗎?有沒有可替代方案?爲何Java沒有使用你說的這種替代方案呢?數組
本節,咱們就來學習另一種數據結構——跳錶,關於跳錶的內容,我將分紅兩節完成,第一節介紹跳錶的演進過程,第二節代碼實現跳錶,並改寫HashMap。數據結構
好了,讓咱們先進入跳錶第一小節的學習。架構
你們都知道數組是能夠支持隨機訪問的,也就是經過下標能夠快速地定位到元素,時間複雜度是O(1)。ide
那麼,這個隨機訪問的特性除了根據下標查找元素,還具備哪些用處呢?源碼分析
試想,若是一個數組是有序的,我要查找某個指定的元素,如何才能作到最快速地查找出來呢?學習
簡單地方法,從頭開始遍歷整個數組,遇到了要查找的元素就返回,好比,查找8這個元素,要走6次才能查找到,要查找10這個元素更誇張,須要8次。優化
因此,這種方式的查找元素的時間複雜度爲O(n)。
快速地方法,由於數組自己是有序的,因此,咱們可使用二分查找,先從中間開始查找,若是指定元素比中間的元素小,再在左半邊查找,若是指定元素比中間元素大,則在右半邊查找,依次進行,直到找到指定元素。好比,查找8這個元素,先定位到中間(7/2=3)的位置,下一次查找讓左指針加1,把4號位置做爲左指針,中間的位置變爲(4+(7-4)/2=5)的位置,查找到8這個元素,一共只須要2次。
使用二分查找,效率提高了不止一星半點,即便最壞的狀況也只須要log(n)的時間複雜度。
上面咱們介紹了有序數組的快速查找,下面咱們再來看看有序鏈表的狀況。
上面是一個有序鏈表,此時,我要查找8這個元素,只能從鏈表頭開始查找,直到遇到8爲止,時間複雜爲O(n),彷佛沒有什麼更好地辦法了。
讓咱們考慮有序數組和有序鏈表的不一樣之處,有序數組之因此可以實現能夠直接定位到中間元素,得意於其能夠經過索引(下標)快速訪問的特性,那麼,咱們給有序鏈表加上索引是否是就能夠實現相似的功能了呢?
答案是確定的,這種具備索引的有序鏈表就是跳錶,下面有請跳錶登場。
第一個問題:怎麼給有序鏈表加索引呢?
這裏,須要增長一個「層」的概念,假設原始鏈表的層級爲0,那麼,在其中選擇一些元素向上延伸,造成第1層索引,一樣地,在第1層索引的基礎上,再選擇一些元素向上延伸,造成第2層索引,直到你以爲索引的層數差很少了爲止,沒錯,跳錶就是這麼隨意,你滿意就好^^
假設,針對上面的有序鏈表,我加了這麼一些索引:
第二個問題:從哪開始訪問這個跳錶呢?6?3?1?9?
好像都不行,因此,還要增長一個特殊的節點——頭節點,放在0號元素的前面,好比,上面的跳錶增長頭節點以後的樣子以下:
此時,只要從h2這個節點開始,就能很快速地查找到跳錶中的任意一個元素。
好比,要查找8這個元素,h2先向右看一下,咦,是6,比8小,跳到6這個位置,再向右看一下,啊,是9了,比8大了,因此,不能跳過去,向下跳一步,跳到第1層6的位置,向右看一下,又是9,不能跳過去,再向下跳一步,到第0層的6,既然,到第0層,那隻能按照鏈表依次日後遍歷了,直到遇到8爲止,整個過程以下:
能夠看到,整個過程就是跳呀跳呀跳,因此得名——跳錶。
這裏的元素個數比較少,可能還看不出太大的優點,試想,若是元素很是多,每兩個元素向上造成一個索引,每兩個索引再向上造成一個索引,最後,就相似於一顆平衡二叉樹了:
能夠看到,每次查找能夠減小一半的搜索範圍,因此,跳錶的查詢時間複雜度爲O(log n)。
可是,實際狀況是不可能使用這種徹底平衡的跳錶的,由於,若是要保持平衡的特性,在插入元素或刪除元素的時候勢必須要作再平衡的操做,這樣就大大地下降了效率,因此,通常地,咱們使用隨機來決定一個元素或者索引要不要產生索引。
第三個問題:索引什麼時候產生呢?
最好的時機莫過於插入元素的時候,由於在插入元素以後的下一步就要立馬使用索引了,爲何這樣說呢?由於不論是插入、刪除仍是查詢,其實,都要先走查詢找到那個元素才能進行下一步操做。說白了,就是無論什麼操做,都要查詢,是查詢就要走索引,要走索引就要先建索引,要建索引那就在插入元素的時候。
OK,下面我將使用一步一圖的方式,帶你領略跳錶建立的完整過程:
初始狀態,只有一個頭節點h0(不,還有一個彤哥讀源碼的水印,調皮^^)。
插入一個元素4,放在h0後面,並隨機決定要不要向上造成索引,結果是不造成索引。
插入一個元素3,從h0開始查找,h0的下一個元素是4,比3大,因此,3放在h0和4之間,而後詢問要不要造成索引,隨機決定說要造成索引,此時,3向上造成索引,同時,h0也要向上造成索引h1,結果以下:
插入一個元素9,從h1開始查找,依次通過h1->3->3->4,都沒有找到位置,最後插入到4後面,並詢問要不要造成索引,隨機決定說我要造成索引,並且我要造成2層索引(最多比當前層數多1),而後就變成了這個樣子:
接着,插入了元素1和7,它們都無驚無喜,沒有造成索引:
插入元素6,根據索引,查找路線爲,h2->h1->3->3->4,咦,發現4下一個是7了,因此,6放在4和7之間,而後,決定要不要造成索引,隨機決定說我要造成索引,並且我也要造成2層索引,這時候就很麻煩了,在造成6這個元素索引的時候,須要修改3->9這條線,還要修改h2->9這條線,生成的結果以下:
後面,插入了元素8和10,都是無驚無險,沒有產生任何索引,因此,最後的結果以下:
能夠看到,跳錶是一個很是隨意的數據結構,即便按照一樣的順序從新插入一遍元素,生成的跳錶也可能徹底不同,任性,因此,我很喜歡跳錶這種數據結構。
第四個問題:上面描述了插入元素的過程,刪除過程是怎麼樣的呢?
刪除過程,首先也要查找到元素,可是,有一點點小區別,很是小的區別,很難描述,好比,要刪除6這個元素,我能不能從h2->6->6->6這個路徑過來呢?
不能,由於從這條路徑過來,刪除第1層的索引6後,沒法修復3->9這條線,因此,刪除元素的時候只能走h2->h1->3->3->4->6這條路徑,且把途中每一層最後通過的索引記住,才能在刪除了6這個元素以後正確地修復各層的索引。
刪除6以後的樣子以下:
咦,講到這裏,我不經想起了Java跳錶ConcurrentSkipListMap中的一個小優化項,在ConcurrentSkipListMap中,不論是查找、插入,仍是刪除,都是走的跟刪除相同的查找路徑,其實,能夠簡單地優化一下,插入和查找的時候徹底能夠走另外一條路徑。
有興趣的同窗能夠扒一下個人源碼分析:死磕 java集合之ConcurrentSkipListMap源碼分析
好了,關於跳錶的理論知識咱們就講解到這裏。
本節,咱們經過一步一圖的方式完整清晰地展現了跳錶查找、插入、刪除元素的全過程,你有沒有Get到呢?能吊打面試官了麼?
然而,不少同窗可能會說「Talk is cheap, Show me the code」,OK,下一節,我就將用代碼的方式給你展示跳錶實現的細節,並使用跳錶改寫HashMap,Next Part 見。
關注公主號「彤哥讀源碼」,解鎖更多源碼、基礎、架構知識。