查詢演算法(上)

goodMorning發表於2019-03-29

簡介

健值對在現代計算機和網路系統應用廣泛(常見如資料庫系統、搜尋引擎等),如果不能快速完成相關操作,大規模應用將無從談起。 下面列舉了健值對部分基礎操作:

  • 查詢 Value get(Key key)
  • 插入 void put(Key key, Value value)
  • 刪除 void delete(Key key)
  • 包含檢測 boolean contains(Key key)
  • 空檢測 boolean isEmpty()
  • 大小 int size() 本文嘗試找著一種實現方式,使得所有的操作都具備對數級別的時間複雜度,本文只關注查詢和插入操作(因為其他介面差不多是這兩個的封裝)。

連結串列

連結串列實現是比較容易想到和理解的方式:將所有元素連線成連結串列:

查詢演算法(上)

查詢和插入

因為是連結串列,所以就是遍歷:

public Value get(Key key) {
  for (Node x = first; x != null; x = x.next) {
    if (key.equals(x.key))
      return x.val;
    }
  return null;
}

public void put(Key key, Value val) {
  for (Node x = first; x != null; x = x.next) {
    if (key.equals(x.key)) {
      x.val = val;
      return;
    }
  }
  first = new Node(key, val, first);
  n++;
}
複製程式碼

優缺點

  • 優點:新增一個節點的時間是固定的
  • 缺點:插入、查詢操作都是線性級別的時間複雜度,運氣不好的話需要遍歷整個連結串列

有序陣列

針對連結串列查詢慢的缺點,我們想到使用二分查詢去解決,即使用一對平行陣列:一個儲存健,一個儲存值,保證存放健的陣列是有序的,再通過陣列的索引去獲取、更新值的陣列:

查詢演算法(上)

查詢和插入

因為是有序陣列,所以使用二分搜尋確定位置,插入新元素的同時,後移後面的元素,並適時增加陣列長度:

public Value get(Key key) {
  if (isEmpty()) return null;
  int i = rank(key); 
  if (i < n && keys[i].compareTo(key) == 0)
    return vals[i];
  return null;
}

public void put(Key key, Value val)  {
  int i = rank(key);
  if (i < n && keys[i].compareTo(key) == 0) {
    vals[i] = val;
    return;
  }

  if (n == keys.length) resize(2*keys.length);

  // !!!後移健值
  for (int j = n; j > i; j--)  {
    keys[j] = keys[j-1];
    vals[j] = vals[j-1];
  }
  keys[i] = key;
  vals[i] = val;
  n++;
}

// 二分搜尋找到key在陣列中的index
public int rank(Key key) {
  int lo = 0, hi = n-1; 
  while (lo <= hi) { 
    int mid = lo + (hi - lo) / 2; 
    int cmp = key.compareTo(keys[mid]);
    if      (cmp < 0) hi = mid - 1; 
    else if (cmp > 0) lo = mid + 1; 
    else return mid; 
  } 
  return lo;
}
複製程式碼

優缺點

  • 優點:檢索複雜度達到對數級別。
  • 缺點:插入操作需要線性級別的時間複雜度,運氣不好的話需要移動整個陣列。

二叉查詢樹

本節我們介紹二叉查詢樹,一種結合連結串列和有序陣列優點的資料結構:

查詢演算法(上)
如圖所示,二叉查詢樹由一些節點連結而成,每個節點儲存了健值、父結點(根結點除外)以及左、右兩個連結(分別指向了左、右結點)。 為了保證快速搜尋,二叉樹的節點是有序的:每一個節點的健大於其左子樹中的所有節點的健,小於其右子樹中所有節點的健

查詢與插入

因為節點是有序的,所以只需要比大小就能判斷是當前節點還是在左右子樹中,遞迴即可:

private Value get(Node x, Key key) {
  int cmp = key.compareTo(x.key);
  if      (cmp < 0) return get(x.left, key);
  else if (cmp > 0) return get(x.right, key);
  else              return x.val;
}

public void put(Key key, Value value) {
  root = put(root, key, value);
}

private Node put(Node curr, Key key, Value value) {
  // 遇到空節點,返回一個新建節點
  if (curr == null) 
    return new Node(key, value);

  int r = key.compareTo(curr.key);
  if (r < 0)
     curr.left = put(curr.left, key, value);
  else if (r > 0)
     curr.right = put(curr.right, key, value);
  else
     curr.value = value;

  return curr;
}
複製程式碼

優缺點

由程式碼可知:二叉樹形狀受節點的插入順序相關,下圖分別展示了最優情況、一般情況以及最差情況:

查詢演算法(上)
顯而易見,二叉樹的最差執行時間取決於樹的高度,而樹的高度又是不可控的,其原因是二叉樹缺乏調整節點位置的能力(只要確定鍵的大小,那麼插入位置就確定了)

總結

文字層層遞進,先後學習使用連結串列、有序陣列和二叉樹3種實現方式,但都不能保證最差情況下對數級別時間複雜度的要求,下篇文章 我們將在二叉樹的基礎上繼續探索。

相關文章