1. 程式人生 > >九、跳錶(Skip List)

九、跳錶(Skip List)

一、概述

跳錶是一種在各個方面都比較優秀的動態資料結構,可支援快速的插入、刪除、查詢操作,甚至可以替代紅黑樹(Red-black Tree)
應用:Redis 中的有序集合(sorted set)是用跳錶實現的。

基本思想

結合 連結串列 和 二分法 的特點,將連結串列進行加工,創造一個二者的結合體:

  • 連結串列從頭節點到尾節點是有序的
  • 可以進行跳躍查詢(形如二分法)

例如:我們建立兩層索引(第三層和第二層),若要查詢某個結點,比如d。可以先在第三層進行遍歷,當遍歷的時候,發現結點d在結點a和節點e之間,則通過第三層結點a的指標,下降到第二層,繼續遍歷==》結點c和節點e之間==》結點c的指標到第一層 ==》繼續遍歷,找到結點d
在這裡插入圖片描述

當連結串列的長度 n 比較大時,比如 1000、10000 的時候,在構建索引之後,查詢效率的提升就會非常明顯。

二、分析

1、時間複雜度分析

如果連結串列裡有 n 個結點,會有多少級索引呢?

每兩個結點會抽取一個作為上一級索引的結點,所以第 k 級索引的結點個數是第 k-1 級索引的結點個數的 1/2,那第 k級索引結點的個數就是 n/(2k)。
==》
假設索引有 h 級,最高階的索引有 2 個結點。通過上面的公式,我們可以得到 n/(2h)=2,從而求得 h=log2n-1。如果包含原始連結串列這一層,整個跳錶的高度就是log2n。
==》
在跳錶中查詢某個資料的時候,如果每一層都要遍歷 m 個結點,那在跳錶中查詢一個數據的時間複雜度就是 O(m*logn)。其中 m=3(二分思想)
==》時間複雜度:O(logn)

2、空間複雜度分析

原始連結串列大小為n,每2個結點抽1個,每層索引的結點數為:

n/2, n/4, n/8, ... , 8 , 4 , 2  

結點總和為 n/2 + n/4 + n/8 + … + 8 + 4 + 2 = n - 2
==》空間複雜度為O(n)

3、變形

每兩個結點抽一個結點到上級索引 ==》每三個結點或五個結點,抽一個結點到上級索引

則,第一級索引需要大約 n/3 個結點,第二級索引需要大約 n/9 個結點。每往上一級,索引結點個數都除以 3。通過等比數列求和公式,總的索引結點大約就是 n/3+n/9+…
+9+3+1=n/2。
==》雖然空間複雜度仍為O(n),但是減少了一半索引結點儲存空間

但是實際應用中,不必太在意索引佔用的額外空間。

三、操作

1、動態插入和刪除

  • 插入、刪除操作的時間複雜度為 O(logn)

為了保證原始連結串列中資料的有序性,需要先找到要插入的位置(時間複雜度為O(logn)),然後執行插入操作。

刪除操作:若該結點在索引中也有出現,除了要刪除原始連結串列中的結點外,還要刪除索引中的。需要獲得要刪除結點的前驅結點。

2、索引動態更新

當不停地插入向跳錶中插入資料時,如果不更新索引,可能導致某兩個索引結點之間的資料非常多的情況 ==》退化為單鏈表

解決方法:通過隨機函式來維護索引與原始連結串列大小之間的 “平衡性”。也就是說,如果連結串列中結點多了,索引結點就相應地增加一些,避免複雜度退化,以及查詢、插入、刪除操作效能下降。

通過隨機函式來決定該結點插入到哪一層索引中,比如隨機函式生成了 K,則將該結點新增到第一級到第K級這K級索引中。
在這裡插入圖片描述

四、應用

Redis 使用跳錶實現有序集合(其實也包括散列表,此處先忽略掉),而非紅黑樹?

Redis中有序結合支援的核心操作主要是:

  • 插入一個數據;
  • 刪除一個數據;
  • 查詢一個數據;
  • 按照區間查詢資料(eg: 查詢值在[155, 296]區間內)==》時間複雜度:O(logn)
  • 迭代輸出有序序列

跳錶更加靈活,它可以通過改變索引構建策略,有效平衡執行效率和記憶體消耗。