日韩av黄I国产麻豆传媒I国产91av视频在线观看I日韩一区二区三区在线看I美女国产在线I麻豆视频国产在线观看I成人黄色短片

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 >

跳表(skipList)

發布時間:2025/3/15 27 豆豆
生活随笔 收集整理的這篇文章主要介紹了 跳表(skipList) 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

一、為何有skipList這種數據結構的出現

我們知道二分查找算法之所以能達到 O(logn) 這樣高效的一個重要原因在于它所依賴的數據結構是數組,數組支持隨機訪問一個元素,通過下標很容易定位到中間元素。而鏈表是不支持隨機訪問的,只能從頭到尾依次訪問。但是數組有數組的局限性,比如需要連續的內存空間,插入刪除操作會引起數組的擴容和元素移動,鏈表有鏈表的優勢,鏈表不需要先申請連續的空間,插入刪除操作的效率非常高。在很多情況下,數據是通過鏈表這種數據結構存儲的,如果是有序鏈表,真的就沒有辦法使用二分查找算法了嗎?實際上對有序鏈表稍加改造,我們就可以對鏈表進行二分查找。這就是我們要說的跳表。說到底,skipList的出現就是為了實現有序鏈表的二分查找,使有序鏈表的查找速度能夠媲美它的效率和紅黑樹以及 AVL 樹,而刪除和插入的速度又能比他們速度更快。下面我們來看一下,跳表是怎么跳的。這里記住一句話:跳表的查找、插入、刪除的時間復雜度都是 O(logn)

二、跳表的特點及結構

總結就是一句話:給有序的鏈表再加索引,在n級索引的基礎上,還可以再加n+1級索引,直至該級索引的索引點只有2個時,就沒必要再加索引

先看一下跳表的特點

  • 由很多層組成
  • 每一層都是一個有序鏈表
  • 最底層的鏈表包含所有元素
  • 每個元素插入時隨機生成它的leve
  • 如果一個元素出現在第i層的鏈表中,則它在i-1層中也會出現
  • 上層節點可以跳轉到下層
  • 跳表查詢、插入、刪除的時間復雜度為O(log n),與平衡二叉樹接近
  • 跳表的最終結構圖:

    三、跳表相關操作

  • 查找操作,查找的時間復雜度

    查找元素的過程是從最高級索引開始,一層一層遍歷最后下沉到原始鏈表。所以,時間復雜度 = 索引的高度 * 每層索引遍歷元素的個數。
  • 圖中所示,現在到達第 k 級索引,我們發現要查找的元素 x 比 y 大比 z 小,所以,我們需要從 y 處下降到 k-1 級索引繼續查找,k-1級索引中比 y 大比 z 小的只有一個 w,所以在 k-1 級索引中,我們遍歷的元素最多就是 y、w、z,發現 x 比w大比 z 小之后,再下降到 k-2 級索引。所以,k-2 級索引最多遍歷的元素為
    w、u、z。其實每級索引都是類似的道理,每級索引中都是兩個結點抽出一個結點作為上一級索引的結點。
    現在我們得出結論:當每級索引都是兩個結點抽出一個結點作為上一級索引的結點時,每一層最多遍歷3個結點。
    跳表的索引高度 h = log2n,且每層索引最多遍歷 3 個元素。所以跳表中查找一個元素的時間復雜度為 O(3*logn),省略常數即:O(logn)。

  • 空間復雜度
  • 跳表通過建立索引,來提高查找元素的效率,就是典型的“空間換時間”的思想,所以在空間上做了一些犧牲,那空間復雜度到底是多少呢?

    假如原始鏈表包含 n 個元素,則一級索引元素個數為 n/2、二級索引元素個數為 n/4、三級索引元素個數為 n/8 以此類推。所以,索引節點的總和是:n/2 + n/4 + n/8 + … + 8 + 4 + 2 = n-2,空間復雜度是 O(n)。
    如下圖所示:如果每三個結點抽一個結點做為索引,索引總和數就是 n/3 + n/9 + n/27 + … + 9 + 3 + 1= n/2,減少了一半。所以我們可以通過較少索引數來減少空間復雜度,但是相應的肯定會造成查找效率有一定下降,我們可以根據我們的應用場景來控制這個閾值,看我們更注重時間還是空間。

  • 插入數據
  • 插入數據看起來也很簡單,跳表的原始鏈表需要保持有序,所以我們會向查找元素一樣,找到元素應該插入的位置。如下圖所示,要插入數據6,整個過程類似于查找6,整個的查找路徑為 1、1、1、4、4、5。查找到第底層原始鏈表的元素 5 時,發現 5 小于 6 但是后繼節點 7 大于 6,所以應該把 6 插入到 5 之后 7 之前。整個時間復雜度為查找元素的時間復雜度 O(logn)。

    這里有一個需要格外注意的地方:如果只是單純插入數據而不更新索引,跳表最后也會退化成鏈表,如下圖:

    4. randomLevel() 方法

    對于這個問題 ,可能大家普遍想到的方法就是,插入數據后刪除原來的索引,再重新建立索引。那么重建索引的重建索引的時間復雜度是多少呢?因為索引的空間復雜度是 O(n),即:索引節點的個數是 O(n) 級別,每次完全重新建一個 O(n) 級別的索引,時間復雜度也是 O(n) 。造成的后果是:為了維護索引,導致每次插入數據的時間復雜度變成了 O(n)。

    那有沒有其他效率比較高的方式來維護索引呢?假如跳表每一層的晉升概率是 1/2,最理想的索引就是在原始鏈表中每隔一個元素抽取一個元素做為一級索引。換種說法,我們在原始鏈表中隨機的選 n/2 個元素做為一級索引是不是也能通過索引提高查找的效率呢? 當然可以了,因為一般隨機選的元素相對來說都是比較均勻的。如下圖所示,隨機選擇了n/2 個元素做為一級索引,雖然不是每隔一個元素抽取一個,但是對于查找效率來講,影響不大,比如我們想找元素 16,仍然可以通過一級索引,使得遍歷路徑較少了將近一半。如果抽取的一級索引的元素恰好是前一半的元素 1、3、4、5、7、8,那么查找效率確實沒有提升,但是這樣的概率太小了。我們可以認為:當原始鏈表中元素數量足夠大,且抽取足夠隨機的話,我們得到的索引是均勻的。我們要清楚設計良好的數據結構都是為了應對大數據量的場景,如果原始鏈表只有 5 個元素,那么依次遍歷 5 個元素也沒有關系,因為數據量太少了。所以,我們可以維護一個這樣的索引:隨機選 n/2 個元素做為一級索引、隨機選 n/4 個元素做為二級索引、隨機選 n/8 個元素做為三級索引,依次類推,一直到最頂層索引。這里每層索引的元素個數已經確定,且每層索引元素選取的足夠隨機,所以可以通過索引來提升跳表的查找效率。

    那代碼該如何實現,才能使跳表滿足上述這個樣子呢?可以在每次新插入元素的時候,盡量讓該元素有 1/2 的幾率建立一級索引、1/4 的幾率建立二級索引、1/8 的幾率建立三級索引,以此類推,就能滿足我們上面的條件?,F在我們就需要一個概率算法幫我們把控這個 1/2、1/4、1/8 ... ,當每次有數據要插入時,先通過概率算法告訴我們這個元素需要插入到幾級索引中,然后開始維護索引并把數據插入到原始鏈表中。下面開始講解這個概率算法代碼如何實現。
    我們可以實現一個 randomLevel() 方法,該方法會隨機生成 1~MAX_LEVEL 之間的數(MAX_LEVEL表示索引的最高層數),且該方法有 1/2 的概率返回 1、1/4 的概率返回 2、1/8的概率返回 3,以此類推。

  • randomLevel() 方法返回 1 表示當前插入的該元素不需要建索引,只需要存儲數據到原始鏈表即可(概率 1/2)
  • randomLevel() 方法返回 2 表示當前插入的該元素需要建一級索引(概率 1/4)
  • randomLevel() 方法返回 3表示當前插入的該元素需要建二級索引(概率 1/8) randomLevel() 方法返回 4 表示當前插入的該元素需要建三級索引(概率1/16)
  • 。。。以此類推
  • 所以,通過 randomLevel() 方法,我們可以控制整個跳表各級索引中元素的個數。重點來了:randomLevel() 方法返回 2 的時候會建立一級索引,我們想要一級索引中元素個數占原始數據的 1/2,但是 randomLevel() 方法返回 2 的概率為 1/4,那是不是有矛盾呢?明明說好的 1/2,結果一級索引元素個數怎么變成了原始鏈表的 1/4?我們先看下圖,應該就明白了。

    假設我們在插入元素 6 的時候,randomLevel() 方法返回 1,則我們不會為 6 建立索引。插入 7 的時候,randomLevel() 方法返回3 ,所以我們需要為元素 7 建立二級索引。這里我們發現了一個特點:當建立二級索引的時候,同時也會建立一級索引;當建立三級索引時,同時也會建立一級、二級索引。所以,一級索引中元素的個數等于 [ 原始鏈表元素個數 ] * [ randomLevel() 方法返回值 > 1 的概率 ]。因為 randomLevel() 方法返回值 > 1就會建索引,凡是建索引,無論幾級索引必然有一級索引,所以一級索引中元素個數占原始數據個數的比率為 randomLevel() 方法返回值 > 1 的概率。那 randomLevel() 方法返回值 > 1 的概率是多少呢?因為 randomLevel() 方法隨機生成 1~MAX_LEVEL 的數字,且 randomLevel() 方法返回值 1 的概率為 1/2,則 randomLevel() 方法返回值 > 1 的概率為 1 - 1/2 = 1/2。即通過上述流程實現了一級索引中元素個數占原始數據個數的 1/2。

    同理,當 randomLevel() 方法返回值 > 2 時,會建立二級或二級以上索引,都會在二級索引中增加元素,因此二級索引中元素個數占原始數據的比率為 randomLevel() 方法返回值 > 2 的概率。 randomLevel() 方法返回值 > 2 的概率為 1 減去 randomLevel() = 1 或 =2 的概率,即 1 - 1/2 - 1/4 = 1/4。OK,達到了我們設計的目標:二級索引中元素個數占原始數據的 1/4。

    以此類推,可以得出,遵守以下兩個條件:

  • randomLevel() 方法,隨機生成 1~MAX_LEVEL 之間的數(MAX_LEVEL表示索引的最高層數),且有 1/2的概率返回1、1/4的概率返回 2、1/8的概率返回 3 …
  • randomLevel() 方法返回 1 不建索引、返回2建一級索引、返回 3建二級索引、返回 4 建三級索引 …
  • 就可以滿足我們想要的結果,即:一級索引中元素個數應該占原始數據的 1/2,二級索引中元素個數占原始數據的 1/4,三級索引中元素個數占原始數據的 1/8 ,依次類推,一直到最頂層索引。

    randomLevel() 方法代碼:

    // 理論來講,一級索引中元素個數應該占原始數據的 50%,二級索引中元素個數占 25%,三級索引12.5% ,一直到最頂層。// 因為這里每一層的晉升概率是 50%。對于每一個新插入的節點,都需要調用 randomLevel 生成一個合理的層數。// 該 randomLevel 方法會隨機生成 1~MAX_LEVEL 之間的數,且 :// 50%的概率返回 1// 25%的概率返回 2// 12.5%的概率返回 3 ...private int randomLevel() {int level = 1;while (Math.random() < SKIPLIST_P && level < MAX_LEVEL) {level += 1;}return level;}

    完整的插入流程:

    整個插入過程的路徑與查找元素路徑類似, 每層索引中插入元素的時間復雜度 O(1),所以整個插入的時間復雜度是 O(logn)。

  • 刪除數據

    刪除元素的時間復雜度:
    刪除元素的過程跟查找元素的過程類似,只不過在查找的路徑上如果發現了要刪除的元素 x,則執行刪除操作。跳表中,每一層索引其實都是一個有序的單鏈表,單鏈表刪除元素的時間復雜度為 O(1),索引層數為 logn 表示最多需要刪除 logn 個元素,所以刪除元素的總時間包含 查找元素的時間 加 刪除 logn個元素的時間 為 O(logn) + O(logn) = 2 O(logn),忽略常數部分,刪除元素的時間復雜度為 O(logn)。
  • 本文轉自

    總結

    以上是生活随笔為你收集整理的跳表(skipList)的全部內容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。