哈夫曼編碼 (Huffman Coding)
Published: Sat Feb 21 2026 | Modified: Sat Feb 07 2026 , 3 minutes reading.
哈夫曼編碼:簡潔之樹
演算法背後的故事:摩爾斯電碼問題
1836 年,塞繆爾·摩爾斯發明了一種透過電線發送資訊的代碼。他注意到 ‘E’ 是英語中最常見的字母,而 ‘Z’ 非常罕見。 所以他把 ‘E’ 設為一個點 (.)。把 ‘Z’ 設為一個複雜的序列 (--..)。 透過讓常見的東西變短,罕見的東西變長,他節省了時間。
1952 年,大衛·哈夫曼 (David Huffman) 從數學上形式化了這個想法。他證明,如果妳根據頻率構建一種特定類型的二叉樹,妳可以為任何數據集創建最優的無前綴編碼。
這就是 哈夫曼編碼。它是 ZIP 檔案如此小巧以及 JPEG 圖像加載如此之快的原因。
為什麼需要它?
- 檔案壓縮: ZIP, GZIP, PNG, JPEG, MP3 都在最後一步使用哈夫曼編碼(或其變體)來榨乾每一個位元。
- 節省頻寬: 在網路上發送更少的數據意味著更快的網站和更低的成本。
演算法是如何「思考」的
這個演算法是一個自底向上的構建者。
- 統計頻率: ‘A’: 5, ‘B’: 9, ‘C’: 12, ‘D’: 13, ‘E’: 16, ‘F’: 45。
- 優先佇列: 將所有字元放入一個按頻率排序的最小堆積中。
- 合併:
- 挑出兩個最小的節點(5 和 9)。
- 將她們合併成一個新節點(總和:14)。
- 把 14 放回堆積中。
- 重複: 不斷合併兩個最小的,直到只剩下一個節點(根)。
- 分配代碼:
- 左分支 = ‘0’
- 右分支 = ‘1’
最高頻的字元最終會靠近頂部(代碼短)。罕見的字元最終會深埋在底部(代碼長)。
工程決策:無前綴編碼
哈夫曼編碼的一個關鍵屬性是沒有代碼是另一個代碼的前綴。
- 如果 ‘E’ 是
0,而 ‘A’ 是01,當解碼器看到0時會困惑……這是 ‘E’ 還是 ‘A’ 的開始? - 在哈夫曼樹中,如果
0被 ‘E’ 佔用了,那麼就沒有其他代碼會以0開頭。這允許在沒有分隔符的情況下進行即時、無歧義的解碼。
實作參考 (Python)
import heapq
from collections import Counter
class Node:
def __init__(self, char, freq):
self.char = char
self.freq = freq
self.left = None
self.right = None
# 優先佇列需要的比較運算符
def __lt__(self, other):
return self.freq < other.freq
def build_huffman_tree(text):
# 1. 統計頻率
freq = Counter(text)
# 2. 構建最小堆積
heap = [Node(char, f) for char, f in freq.items()]
heapq.heapify(heap)
# 3. 合併節點
while len(heap) > 1:
left = heapq.heappop(heap)
right = heapq.heappop(heap)
# 創建內部節點 (char 為空)
merged = Node(None, left.freq + right.freq)
merged.left = left
merged.right = right
heapq.heappush(heap, merged)
return heap[0] # 根節點
def generate_codes(node, prefix="", code_map={}):
if node:
if node.char is not None:
code_map[node.char] = prefix
generate_codes(node.left, prefix + "0", code_map)
generate_codes(node.right, prefix + "1", code_map)
return code_map
# 使用範例
text = "BEEP BOOP BEER!"
root = build_huffman_tree(text)
codes = generate_codes(root)
print(f"編碼表: {codes}")
# 像 'E' 這樣的常用字母將擁有更短的二進位串小結
哈夫曼編碼教會我們:平等是低效的。將每個字元視為平等(都用 8 位元)是在浪費空間。透過擁抱不平等——給予高頻者特權(短代碼),給予罕見者負擔(長代碼)——我們實現了最高效的系統。它提醒我們,背景(頻率)定義了價值。
