動機
固定長度編碼為每個符號分配相同數目的 bit,做法簡單,卻沒有利用資料的頻率差異。若 a 佔一份檔案接近一半,而 f 只偶爾出現,兩者使用同樣長的 codeword 便會浪費空間。Huffman coding 結合 binary tree 與 min-priority queue,讓常見符號走較短的路徑,罕見符號則承擔較長的路徑。
分析時要分清三個問題。第一,建構:為何每輪都要合併目前頻率最小的兩棵 subtree。第二,可解碼性:可變長度 codeword 連在一起而沒有分隔符,為何仍可逐個還原。第三,成本:在指定頻率模型下,整段資料究竟需要多少 bit。只有一棵看似合理的樹而沒有這三項檢查,答案並不完整。
Heap 的同一種用法亦會出現在合併 k 條已排序 linked list。下一個輸出必定是各 list 當前 head 中的最小者;min-heap 只保存這些候選項,毋須每輸出一次便重新掃描全部 k 個 heads。兩個問題的共通點,都是維護一個規模較小、只含目前合資格最小項目的 frontier。
定義
定義
頻率模型
設 alphabet 有 m 個不同符號。符號 x 的權重 w(x) 可以是正整數出現次數,也可以是 probability;若採用 probability,總和應為 1。把所有權重同時乘以相同正數,不會改變每輪哪兩項最小,因此不會改變 Huffman merge 的結構。
定義
Binary prefix code
Binary code 為每個符號分配一條有限 bit string。若沒有任何完整 codeword 是另一個 codeword 的 prefix,便稱為 prefix-free。在 prefix-code tree 中,原符號只放在 leaves;慣例把 left edge 標作 0、right edge 標作 1,由 root 走到 leaf 所讀到的 edge labels 就是該符號的 code。
定義
加權碼長
若符號 x 的 codeword 長度為 ell(x),加權碼長為 L = sum_x w(x) ell(x)。權重若是 probabilities,L 是每個符號所需 bit 數的期望值;權重若是出現次數,L 就是 encoded payload 的確實 bit 數。這個數字不包括 code tree、frequency header、檔案 metadata 或補齊整個 byte 的成本;分析完整檔案格式時必須另外計算。
每輪合併兩個 minimum
先為每個符號建立一個以頻率作 key 的 leaf,全部放進 min-heap。每輪 deleteMin 兩次,把兩棵 tree 接到一個新 internal node 之下;新 node 的權重是兩者之和,再把它 insert 回 heap。每次 merge 令 heap item 數量減一,所以 m 個 leaves 恰好需要 m - 1 次 merges。
HUFFMAN(symbols with positive weights)
H = a min-heap containing one leaf per symbol
while size(H) > 1
x = deleteMin(H)
y = deleteMin(H)
z = a new internal node with weight x.weight + y.weight
z.left = x
z.right = y
insert(H, z)
return deleteMin(H)
把較輕的 x 放在左邊可令 trace 容易重現,但交換任何 internal node 的左右 children 仍然有效,只會改變 bit string,不會改變 codeword length 或儲存成本。若有相同權重,heap 的 tie-breaking 也可能產生不同而同樣合法的樹,所以 Huffman code table 並不一定唯一。
過程中的 invariant 是:heap 內每個 item 都是一棵 subtree 的 root;各 subtree 的 leaves 互不重疊,合起來恰好是整個 alphabet。Merge 只會以 union 取代兩個互不重疊部分,而新 root 的權重正是下方所有 leaves 的總權重。最後只剩一個 root 時,每個原符號都恰好出現一次。
若輸入長度為 N,掃描並統計 frequencies 需要 O(N)。把 m 個 leaves bottom-up 建成 heap 可用 O(m);之後 m-1 輪各有兩次 deleteMin 和一次 insert,每項最多 O(log m),所以建樹為 O(m log m)。遍歷最終 tree 記錄所有 codes 為 O(m),完整 preprocessing 因而是 O(N + m log m);若題目已給 frequency table,便不用 O(N) 掃描。Encoding 對每個輸入符號做一次 code-table lookup,另花與實際輸出 bit 數成比例的時間;decoding 則對讀取的 bit 數是線性。
若 alphabet 只有一個符號,merge loop 會得到單一 leaf 和空路徑。實際檔案格式通常約定用 0 表示該符號,令重複出現仍有明確表示。本節六符號例題毋須使用這個 edge case。
定理 / 命題
定理
Leaf code 必定 prefix-free
若每個 source symbol 都標在 binary tree 的 leaf,並以 root-to-leaf edge sequence 作為 code,便沒有任何 symbol codeword 可以成為另一個 symbol codeword 的 prefix。
定理
Prefix-code stream 的符號邊界唯一
已知同一棵 prefix-code tree 時,任何由其 leaf codewords 串接而成的 bit string,都只有一種由左至右的 source-symbol decoding。
定理
Merge-sum identity
對一棵由反覆合併兩個 weighted roots 而成的 tree,所有 leaves 的 weighted external path length,等於每次 merge 所建立 internal node 權重的總和。即 sum_x w(x) ell(x) 與全部 combined weights 的總和相同。
以上命題足以支持本節的 decoding 與數值核算。這個建構就是 Huffman greedy algorithm,但完整證明它在所有 binary prefix trees 中使 weighted length 最小,需要另一套 exchange argument,並不屬於本節課程來源所支持的範圍;以下推導不會假裝已完成該 optimality proof。
證明思路
第一個命題可用反證法理解。假設某 leaf code 是另一個 leaf code 的 prefix,沿較短 code 由 root 行走後已到達第一個 leaf,卻仍須沿更多 edges 才到第二個符號。Leaf 沒有 child,故不可能。這也解釋了為何 source symbols 必須放在 leaves,而不能放在 internal nodes。
解碼時由 root 開始逐 bit 行走。第一次到達 leaf 時,第一個符號已被強制決定:若提早停止,internal node 便要代表 code;若繼續走,該 leaf code 又會成為較長 code 的 prefix。輸出符號、回到 root,對餘下 codewords 重複即可。若 bit stream 結束時仍停在 internal node,它是截斷或無效資料,而不是另一種合法解碼。
要證明 merge-sum identity,觀察權重為 p 與 q 的兩個 roots 被接到新 parent 後,兩棵 subtree 內每個 leaf 的 depth 都增加一。因此 weighted length 的增加恰好是 p+q,也就是新 internal node 的權重。由 depth 全為零的獨立 leaves 開始,把每輪增量相加,就得到最終 weighted external path length。故 code-length sum 與 merge-weight sum 應相等,可用來偵測算術或 tree trace 錯誤。
對 k-way merge,invariant 是 heap 恰好保存每條非空 list 的第一個未輸出 element。因為每條 list 已排序,該 head 不大於同一 list 其餘未輸出 elements;所以 heap minimum 亦是全體剩餘元素的 global minimum。刪除它並把同一 list 的 successor 放回 heap,便恢復 invariant。
例題詳解
例題
完整六符號 Huffman 建構
Tutorial 給出的百分比頻率是:
| 符號 | a | b | c | d | e | f |
|---|---|---|---|---|---|---|
| 頻率 | 45 | 13 | 12 | 16 | 9 | 5 |
先核對 45 + 13 + 12 + 16 + 9 + 5 = 100。Ascending heap view 是 5, 9, 12, 13, 16, 45。五次 merges 完整如下:
f:5 + e:9 -> 14;c:12 + b:13 -> 25;(f,e):14 + d:16 -> 30;(c,b):25 + ((f,e),d):30 -> 55;a:45 + 55 -> 100。
每次 insert 後都要從整個 heap 再找兩個 minima,早前建立的 internal node 亦是候選。把較輕 child 放左,並標 left=0、right=1,完整 tree 為:
(100)
0 / \ 1
a:45 (55)
0 / \ 1
(25) (30)
0/ \1 0/ \1
c:12 b:13 (14) d:16
0/ \1
f:5 e:9
由 root 讀到各 leaf:
| 符號 | code | 長度 | 頻率 |
|---|---|---|---|
a | 0 | 1 | 45 |
b | 101 | 3 | 13 |
c | 100 | 3 | 12 |
d | 111 | 3 | 16 |
e | 1101 | 4 | 9 |
f | 1100 | 4 | 5 |
Length pattern 通過完整樹檢查:2^-1 + 3(2^-3) + 2(2^-4) = 1。即使交換 children,bits 會不同,長度 1,3,3,3,4,4 與成本仍不變。
例題
以三種方法核對 224,000 bits
100,000 個 characters 對應 counts 45,000、13,000、12,000、16,000、9,000、5,000。平均碼長為
0.45(1) + 0.13(3) + 0.12(3) + 0.16(3) + 0.09(4) + 0.05(4)
= 0.45 + 0.39 + 0.36 + 0.48 + 0.36 + 0.20 = 2.24 bits/character,因此 payload 是 100,000(2.24) = 224,000 bits。直接用整數 counts 計亦得
45,000 + 39,000 + 36,000 + 48,000 + 36,000 + 20,000 = 224,000。
第三個核對是 merge sum:14 + 25 + 30 + 55 + 100 = 224 bit-percent units,放大至 100,000 characters 同樣是 224,000 bits。三者一致,可同時檢查 frequency、merge 和 code length。
按 tutorial 明確採用的 8-bit ASCII comparison,成本為 100,000(8)=800,000 bits,Huffman 節省 576,000 bits,即原 payload 成本的 72%。六個符號最短固定長度需要 ceil(log2 6)=3 bits,所以固定編碼用 300,000 bits;Huffman 節省 76,000 bits,即固定長度成本的 25 1/3%。
例題
沒有分隔符的唯一解碼
用上表解讀 1000101。由 root 讀 100 到 leaf c,輸出後回 root;下一個 0 到 a;餘下 101 到 b。唯一分割是 100 | 0 | 101,結果為 cab。把 bit stream 強行分成等長 groups 並不適用於 variable-length code。
例題
三條 sorted lists 的 heap trace
設 L1=[1,7,10]、L2=[2,3,11]、L3=[4,5,6]。先把 (1,L1)、(2,L2)、(4,L3) 放進 min-heap。刪除 1 後插入 7;刪除 2 後插入 3;刪除 3 後插入 11;刪除 4 後插入 5。繼續同一規則,輸出為 1,2,3,4,5,6,7,10,11。
MERGE_K_SORTED_LISTS(lists)
H = empty min-heap
for each nonempty list i
insert(H, (lists[i].head.value, i, lists[i].head))
output = empty list
while H is not empty
(value, i, node) = deleteMin(H)
successor = node.next
append value to output
if successor exists
insert(H, (successor.value, i, successor))
return output
若 k 條 lists 合共有 n 個 elements,heap 最多只有 k 個 heads。每個輸出有一次 deleteMin 及至多一次 insert,各為 O(log k),故 k>=2 時總時間是 O(n log k),auxiliary heap space 是 O(k)。逐次掃描全部 heads 會是 O(nk);把資料串接後重新 sorting 則是 O(n log n)。當 k=1,直接複製唯一 list 為 O(n)。
常見錯誤
- 合併兩個最大 weights,或只按原始 frequency list 繼續選擇。每輪都必須在目前 heap 中選兩個 minima,包括剛 insert 的 internal nodes。
- 由 leaf 向 root 倒讀 code。Code 是 root-to-leaf path,反轉後不是同一 bit string。
- 把 source symbol 放在 internal node。它的 code 便可能成為 descendant code 的 prefix,破壞無分隔符解碼。
- 斷言 frequency 較高者的 code 必定嚴格較短。Tie 或 tree shape 可令多個符號同長;真正長度由最終 tree 決定。
- 只數 codeword 數目而沒有計 weighted length;另要記住
224,000bits 是 payload,未包括 tree/header overhead。 - 認為六個 symbols 可用兩個 fixed bits。
2^2=4只表示四種值,六個 symbols 必須用三個 bits。 - 把
k條 list 的所有 elements 一次放入 heap。只有每條非空 list 的 current head 合資格,heap size 應不超過k。
總結
Huffman construction 以 min-heap 反覆合併兩個 minimum-weight roots。原符號全在 leaves,因此 root-to-leaf codes 是 prefix-free,連續 codewords 亦可唯一解碼。六符號例題的 merges 是 5+9=14、12+13=25、14+16=30、25+30=55、45+55=100;碼長為 1,3,3,3,4,4,100,000-character payload 恰好使用 224,000 bits。相對指定的 8-bit ASCII 節省 576,000 bits,相對最短三-bit固定編碼節省 76,000 bits。
Heap 亦可合併 k 條 sorted lists。每條非空 list 只提供一個 current head,令 n 次輸出各以 O(log k) 找 global minimum,總時間 O(n log k),額外空間 O(k)。
練習
- 由 weights
5,9,12,13,16,45開始,列出每次 Huffman merge,以及 insert 後 heap 餘下的 weights。 - 使用完整建構中的 code table,解讀
11011100111,並標出 leaf boundaries。 - 分別以 weighted code lengths 和 merge-sum identity 重算六符號 payload;再寫出相對 8-bit ASCII 與六符號最短 fixed-length code 的節省量。
- 判斷
{0,01,11}是否 prefix-free,並解釋 decoding 後果。 - 為合併
k條 sorted linked lists 寫出 heap 方法,說明 invariant、以總長度n表示的時間複雜度,以及 auxiliary space。
答案與解答
解答 · 1. Merge trace
依次為:5+9=14,餘下 12,13,14,16,45;12+13=25,餘下 14,16,25,45;14+16=30,餘下 25,30,45;25+30=55,餘下 45,55;45+55=100,只剩 root。六個 leaves 要減至一個 root,所以共有五次 merges。
解答 · 2. Decoding
唯一 leaf split 是 1101 | 1100 | 111,分別到達 e、f、d,所以結果為 efd。
解答 · 3. 儲存核算
Weighted sum 是 45(1)+13(3)+12(3)+16(3)+9(4)+5(4)=224 bits/100 characters;merge sum 亦為 14+25+30+55+100=224。乘 1,000 得 224,000 bits。ASCII 是 800,000,節省 576,000;三-bit fixed code 是 300,000,節省 76,000 bits。
解答 · 4. Prefix 檢查
它不是 prefix-free,因為 0 是 01 的 prefix。Decoder 看到第一個 0 時,無法單靠該 bit 決定立即輸出第一個符號,還是等待下一個 bit;leaf codes 正是避免這種歧義。
解答 · 5. Multiway merge
把每條非空 list 的第一個 node 連同來源標記放進 min-heap。重複刪除 minimum、把它接到 output,若同一 list 還有 successor 便 insert。Invariant 是 heap 恰好有每條非空 list 的一個 next candidate。Heap size 至多 k,故 n 次刪除和至多 n 次插入在 k>=2 時共 O(n log k),額外 heap space 為 O(k)。