簡介
健值對在現代計算機和網路系統應用廣泛(常見如資料庫系統、搜尋引擎等),如果不能快速完成相關操作,大規模應用將無從談起。 下面列舉了健值對部分基礎操作:
- 查詢
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種實現方式,但都不能保證最差情況下對數級別時間複雜度的要求,下篇文章 我們將在二叉樹的基礎上繼續探索。