跳至主要内容

Hash Table / Map

雜湊表(Hash Table / Hash Map)是一種用「鍵(Key)」直接對應到「值(Value)」的資料結構,即 key-value pair 的形式儲存資料。

它最大的特色是讀取、新增、刪除的平均時間複雜度都是 O(1),速度超快,在 JavaScript 裡 Object 或 Map 就是用 Hash Table 實作,在 Python 則是 Dictionary。

白話理解

想像查電話簿或字典,你不會從第一頁翻到最後一頁找某個名字或單字,而是直接翻到對應的字母、對應的區塊。Hash Table 的概念就是這樣:用一個 Hash Function 把 Key「算」出它應該放在哪個位置,之後要找的時候,一樣算一次就能直接跳過去,不用整本翻過一遍。

複雜度​

前面提到讀取、新增、刪除的平均時間複雜度都是 O(1)

  • Access - O(1)
  • Insertion - O(1)
  • Removal - O(1)

Hash Function & Collision​

Hash Table 的運作核心是 Hash Function。當你存入一個 Key 時,Hash Function 會把這個 Key 轉成一個數字(記憶體 index),並把 Value 存進該 index 對應的陣列格子(Bucket)中。

可是記憶體陣列的長度是有限的,而 Key 的組合是無限的。當兩個不同的 Key 經過 Hash Function 計算後,得到同一個 index,這就叫做碰撞(Collision),白話地說就是兩個不同的 key 存到同一個地方去。

當遇到 collision 時,常用以下方式處理:

  • Separate Chaining:在每個格子裡放一個鏈結串列(Linked List)或陣列(Array),發生 collision 時就直接把資料往後排。
  • Open Addressing:如果發現格子被佔用了,就往後找下一個空的格子坐。

實作​

設計 Hash Function 是一門艱深的學問,好的 hash 具備以下條件:

  • 快速,計算時間是 constant time
  • 不容易 collision,output 不會集中在某一特定的 index,而是均勻分散
  • 同樣的 input 會得到同樣的 output

這邊不仔細探討如何設計優良的 Hash Function,用最簡易的方式實作。

class HashTable:
def __init__(self, size=50):
# 初始化一個固定大小的陣列作為儲存空間
self.key_map = [None] * size

# 1. 內部雜湊函數:將 Key 轉成 array index (Key 的長度))
def _hash(self, key):
total = 0
weird_prime = 31 # 使用質數可以減少雜湊衝突的機率

for char in key[:100]:
value = ord(char) - 96
total = (total * weird_prime + value) % len(self.key_map)
return abs(total)

# 2. 新增或修改資料:O(1)
def set(self, key, value):
index = self._hash(key)

# 如果該位置是空的,先初始化一個 array(Separate Chaining)
if not self.key_map[index]:
self.key_map[index] = []

# 檢查 Key 是否已經存在,存在就更新 Value
for pair in self.key_map[index]:
if pair[0] == key:
pair[1] = value
return

# 不存在就直接放入內容 [key, value]
self.key_map[index].append([key, value])

# 3. 讀取資料:O(1)
def get(self, key):
index = self._hash(key)
bucket = self.key_map[index]

if bucket:
# 在 array 尋找對應的 key
for pair in bucket:
if pair[0] == key:
return pair[1] # 回傳 value
return None # 找不到回傳 None

# 4. 刪除資料:
# 平均 - O(1)
# 最差 - O(n),當所有資料都衝突在同一個桶子時
def remove(self, key):
index = self._hash(key)
bucket = self.key_map[index]

if bucket:
for i, pair in enumerate(bucket):
# 找到對應的 key
if pair[0] == key:
removed_pair = bucket.pop(i) # 將該資料從 array 中移除
return removed_pair[1] # 回傳被刪除的 value
return None # 找不到該 key 則回傳 None

# 5. 獲取所有鍵(Keys):O(m) - m 為 hash table 分配的總容量 (Size)
def keys(self):
keys_array = []
for bucket in self.key_map:
# 如果桶子裡有資料,就遍歷裡面的 array
if bucket:
for pair in bucket:
keys_array.append(pair[0])
return keys_array

# 6. 獲取所有值(Values):O(m) - 同時幫你過濾掉重複的值
def values(self):
values_array = []
for bucket in self.key_map:
if bucket:
for pair in bucket:
value = pair[1]
# 避免塞入重複的 Value(可依需求調整是否要不重複)
if value not in values_array:
values_array.append(value)
return values_array

適用情況​

當資料是鍵值對(Key-Value pairs),且你需要像查字典一樣,輸入一個 Key 就要「瞬間(O(1))」拿到資料時。

  • 快取(Cache / Session)儲存:例如把 API 回傳的結果存起來,Key 是 url,Value 是 data。下次請求時直接拿,不用重新發請求。
  • 根據 ID 查找資料(Lookup Dictionary):如果你有 10 萬筆使用者資料,需要頻繁用 userId 去找使用者的詳細資料。用 Array 找則要從頭到尾翻(O(n)),用 Hash Map 可以瞬間秒找(O(1))。
  • 計數器 / 頻率統計:例如「統計一篇文章中,每個單字出現了幾次」。Key 存單字,Value 存次數。
  • 設定檔(Configuration):例如系統環境設定,環境變數如 {theme: "dark", language: "zh-TW"}。
  • 搭配 Prefix Sum 或 Sliding Window:例如記錄「某個前綴和出現過幾次」來判斷子陣列和是否等於特定值,或記錄視窗內元素的出現次數。

常見誤區​

  • 誤以為 O(1) 是保證值,而非平均值:Hash Table 的 O(1) 是「平均」時間複雜度。如果 Hash Function 設計不良,導致大量資料集中碰撞(Collision)在同一個 Bucket,最壞情況下讀取、新增、刪除都會退化成 O(n)。
  • 誤以為 Key 的順序有保障:在部分語言或版本中,Hash Table 內部的資料順序不一定等於你塞入的順序(雖然現代 JavaScript 的 Object/Map 已經會保留插入順序,但這是語言的額外保證,不是 Hash Table 這個資料結構本身的特性),如果程式邏輯依賴特定順序,應改用 Array 或明確排序。
  • 用可變(Mutable)物件當 Key:例如直接把一個 array 或物件當作 Key,之後如果內容被修改,會導致算出來的 Hash 值改變,反而找不到原本存進去的資料。Key 應盡量使用不可變的型別,例如字串或數字。