跳錶--怎麼讓一個有序連結串列能夠進行"二分"查詢?

天份&發表於2021-10-06

對於一個有序陣列,如果要查詢其中的一個數,我們可以使用二分查詢(Binary Search)演算法,將它的時間複雜度降低為O(logn).那查詢一個有序連結串列,有沒有辦法將其時間複雜度也降低為O(logn)呢?

跳錶(skip list),全稱為跳躍連結串列,實質上就是一種可以進行二分查詢的有序連結串列,它允許快速查詢、插入和刪除有序連結串列

跳錶使用的前提是連結串列有序,就像二分查詢也要求有序陣列

怎麼理解跳錶

比如我們有一個原始有序連結串列,如下圖所示。

要查詢其中值為20的元素,之前都是採取按順序進行遍歷的方法,但這樣做時間複雜度就變成了O(n).怎樣才能提高效率呢?我們可以通過對連結串列建立一級索引,查詢的時候先遍歷索引,通過索引找到原始層繼續遍歷。索引如下圖所示

那麼查詢20的過程就變成了先使用索引遍歷 2 -> 7 -> 12 -> 20,然後順著索引連結串列的結點向下找到原始連結串列的結點20.之前需要遍歷7次,現在需要遍歷5次。在資料量小的時候跳錶效能優化並不明顯,但當有序連結串列包含大量資料時,結點的訪問次數大致會減少一半。

現在我們新增兩層索引,基於第一層的索引再新增一層,如下圖所示

要查詢20,先在第二層索引上遍歷 2 -> 12 ,然後向下轉到第一層索引遍歷 12 - > 20,最後向下找到原始連結串列的結點20.

這個例子中,原始有序連結串列的結點數量很少,當結點數量很多時,可以抽出更多的索引層級,每一層索引結點的數量都是低層索引的一半。

跳錶複雜度分析

時間複雜度

演算法的執行效率可以通過時間複雜度來衡量,跳錶的時間複雜度是多少呢?我們來分析一下。

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

m的值怎麼計算呢?在上面的例子中,每一層最多隻需要遍歷三個元素,因此m=3,根據時間複雜度的計算規則,高階的常數項也可以省略,因此跳錶中查詢任意資料的時間複雜度就是O(logn)

空間複雜度

每兩個結點中抽一個結點作為上級索引,很明顯,它的空間複雜度為O(n).

?‍♂這是一個典型的空間換時間操作。原始連結串列中儲存的有可能是很大的物件,而索引結點只需要儲存關鍵值和幾個指標,並不需要儲存物件,所以當物件比索引結點大很多時,索引佔用的額外空間就可以忽略了。

高效的插入和刪除

插入操作

向連結串列插入資料的時間複雜度是O(1),但為了保持連結串列資料有序,需要先找到插入結點的前置結點,然後插入資料到前置結點後面,其時間複雜度為O(logn)。假設我們要插入10,需要先找到前置結點9,然後插入10。

刪除操作

刪除的話也是需要先找到要刪除的結點,如果該結點在索引中也有出現的話,索引中的也需要刪除。因為單連結串列中的刪除操作需要拿到要刪除結點的前驅結點,然後通過指標操作完成刪除。所以在查詢要刪除的結點的時候,一定要獲取前驅結點。

動態更新索引

當我們一直往跳錶中插入資料時,兩個索引結點之間的資料可能會變得非常多,在極端情況下,跳錶還會退化成單連結串列,這樣的話跳錶的優勢也就沒有了。

因此我們需要用一些方法來維護索引和原始連結串列之間的平衡,也就是在增加原始連結串列中結點內容的時候適當增加索引的大小。為了維護平衡,跳錶的設計者採用了一種有趣的方法:“拋硬幣”,也就是隨機決定新結點是否建立索引,兩個結點建立一個索引的話,每層的概率為50%。

Java實現跳錶

下面是王爭老師 資料結構與演算法之美 課程中的程式碼

package skiplist;
/**
 * 跳錶的一種實現方法。
 * 跳錶中儲存的是正整數,並且儲存的是不重複的。
 *
 * Author:ZHENG
 */
public class SkipList {
  private static final float SKIPLIST_P = 0.5f;
  private static final int MAX_LEVEL = 16;
​
  private int levelCount = 1;
​
  private Node head = new Node();  // 帶頭連結串列
​
  public Node find(int value) {
    Node p = head;
    for (int i = levelCount - 1; i >= 0; --i) {
      while (p.forwards[i] != null && p.forwards[i].data < value) {
        p = p.forwards[i];
      }
    }
​
    if (p.forwards[0] != null && p.forwards[0].data == value) {
      return p.forwards[0];
    } else {
      return null;
    }
  }
​
  public void insert(int value) {
    int level = randomLevel();
    Node newNode = new Node();
    newNode.data = value;
    newNode.maxLevel = level;
    Node update[] = new Node[level];
    for (int i = 0; i < level; ++i) {
      update[i] = head;
    }
​
    // record every level largest value which smaller than insert value in update[]
    Node p = head;
    for (int i = level - 1; i >= 0; --i) {
      while (p.forwards[i] != null && p.forwards[i].data < value) {
        p = p.forwards[i];
      }
      update[i] = p;// use update save node in search path
    }
​
    // in search path node next node become new node forwords(next)
    for (int i = 0; i < level; ++i) {
      newNode.forwards[i] = update[i].forwards[i];
      update[i].forwards[i] = newNode;
    }
​
    // update node hight
    if (levelCount < level) levelCount = level;
  }
​
  public void delete(int value) {
    Node[] update = new Node[levelCount];
    Node p = head;
    for (int i = levelCount - 1; i >= 0; --i) {
      while (p.forwards[i] != null && p.forwards[i].data < value) {
        p = p.forwards[i];
      }
      update[i] = p;
    }
​
    if (p.forwards[0] != null && p.forwards[0].data == value) {
      for (int i = levelCount - 1; i >= 0; --i) {
        if (update[i].forwards[i] != null && update[i].forwards[i].data == value) {
          update[i].forwards[i] = update[i].forwards[i].forwards[i];
        }
      }
    }
      
    while (levelCount>1&&head.forwards[levelCount]==null){
      levelCount--;
    }
  }
​
  // 理論來講,一級索引中元素個數應該佔原始資料的 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;
  }
​
  public void printAll() {
    Node p = head;
    while (p.forwards[0] != null) {
      System.out.print(p.forwards[0] + " ");
      p = p.forwards[0];
    }
    System.out.println();
  }
​
  public class Node {
    private int data = -1;
    private Node forwards[] = new Node[MAX_LEVEL];
    private int maxLevel = 0;
​
    @Override
    public String toString() {
      StringBuilder builder = new StringBuilder();
      builder.append("{ data: ");
      builder.append(data);
      builder.append("; levels: ");
      builder.append(maxLevel);
      builder.append(" }");
​
      return builder.toString();
    }
  }
}

總結

 

相關文章