4.2 線性雜湊#
概念說明#
線性雜湊(linear hashing)由 Witold Litwin 於 1980 年提出,它捨棄了可擴展雜湊的目錄,用「漸進分裂」達成擴張:表格不一次加倍,而是每次溢位時只分裂一個桶——而且分裂的對象不是溢位的桶,而是按固定順序輪到的桶(由指標 split 指定)。這樣分裂是可預測、增量的,不需要目錄,任何時刻磁碟上就是一個簡單的桶陣列。
核心機制:設目前桶數為 N'(介於 N 與 2N 之間,N 為上一次 2 的冪)。查詢鍵 k 時:
- 若
h(k) mod N' ≥ split(該桶尚未輪到分裂),用h(k) mod N'定位。 - 否則(該範圍已分裂過),用
h(k) mod 2N定位。
插入導致溢位時,把 split 指到的桶分裂成兩個(依 h(k) mod 2N 分配記錄),split 前進一格;split 走到 N 時,一輪完成,N ← 2N,split 歸零。也可以用「負載因子超過門檻」而非溢位來觸發分裂,讓桶不必真的溢位。
演算法步驟#
插入(鍵 k): 1. 以上述規則定位桶,插入記錄(必要時掛溢位頁)。 2. 若負載因子 > 門檻(例如 0.8):分裂 split 桶,split += 1;若 split == N,N *= 2, split = 0。 3. 分裂時把溢位頁中的記錄一併重新分配,常常順便消解其他桶的溢位。
查詢(鍵 k): 1. p = h(k) mod N';若 p < split 則 p = h(k) mod (2N)。 2. 讀取桶 p 與其溢位鏈。
Python 範例#
class LinearHash:
def __init__(self, bucket_size=3, load=0.8):
self.N = 4 # 目前桶陣列大小(2 的冪)
self.split = 0 # 下一個要分裂的桶
self.count = 0
self.buckets = {i: [] for i in range(self.N)}
self.bs = bucket_size
self.load = load
self.io = 0
def _addr(self, key):
self.io += 1
p = key % self.N
if p < self.split:
p = key % (2 * self.N)
return p
def insert(self, key):
p = self._addr(key)
self.io += 1
self.buckets.setdefault(p, []).append(key)
self.count += 1
if self.count / self.N > self.load: # 觸發分裂
old = self.buckets.pop(self.split, [])
self.io += 1
b0, b1 = self.split, self.split + self.N
self.buckets[b0] = [k for k in old if k % (2*self.N) == b0 % (2*self.N)]
self.buckets[b1] = [k for k in old if k % (2*self.N) == b1 % (2*self.N)]
self.split += 1
if self.split == self.N:
self.N *= 2
self.split = 0
def search(self, key):
p = self._addr(key)
self.io += 1
return key in self.buckets.get(p, [])
h = LinearHash(bucket_size=3)
for k in range(1, 31):
h.insert(k)
print("桶數:", h.N, "split:", h.split, "最大桶長:",
max(len(v) for v in h.buckets.values()))
print("查詢 17:", h.search(17), "查詢 99:", h.search(99))
print("總 I/O:", h.io)
複雜度與 I/O 成本分析#
- 查詢:平均 1~2 次 I/O;桶有溢位鏈時加上鏈長。
- 插入:1 次寫 + 分裂時約 3 次 I/O(讀舊桶、寫兩新桶),且分裂成本被「攤提」到多次插入上——任意時刻最多只有一個桶在分裂中。
- 空間利用率可控(由負載因子門檻決定),無目錄開銷,這正是許多儲存引擎偏好它的原因。
- 缺點:分裂的是「輪到的桶」而非「溢位的桶」,短期內溢位鏈可能存在;需要溢位頁處理。
練習#
- 以 N=4、load=0.8 依序插入 1..12,列出每次分裂發生的時機與 split 的變化。
- 比較線性雜湊與可擴展雜湊在「查詢 I/O 次數」與「插入尖峰延遲」上的差異。
- 修改程式改用「溢位即分裂」的觸發策略(分裂仍按 split 順序),觀察桶長分布變化。