基於 gzip-1.2.4 源碼,從 LZ77 + Huffman 雙階段原理到滑動窗口、哈希鏈、延遲匹配的工程化實現
原始數據
│
▼
┌─────────┐ LZ77 編碼 (滑動窗口 + 重複串替換)
│ LZ77 │ ──────────────────► 字面量 + <長度,距離> 對
└─────────┘
│
▼
┌──────────┐ Huffman 編碼 (最優前綴碼)
│ Huffman │ ──────────────────► 位流
└──────────┘
│
▼
壓縮數據 (zlib/gzip/PNG 封裝格式)
window[32768]。strstart 指向當前待編碼位置,lookahead 為可用數據長度。MIN_MATCH = 3。因 <長度,距離> 對需 23 位 (長度 8 位 + 距離 15 位),3 字節 = 24 位,始有壓縮收益。核心數據結構:
// 全局靜態數組
ush *head; // head[hash] = 最新匹配該哈希值的窗口位置 (鏈表頭)
Pos *prev; // prev[window_pos] = 同哈希值的前一個位置 (鏈表節點)
哈希函數 (取窗口前 3 字節):
#define H_SHIFT ((HASH_BITS + MIN_MATCH - 1) / MIN_MATCH) // 5
#define HASH(h, c) (((h) << H_SHIFT) ^ (c)) & HASH_MASK
// 初始化
ins_h = 0;
for (i = 0; i < MIN_MATCH - 1; i++)
UPDATE_HASH(ins_h, window[i]); // 預計算前 2 字節哈希
// 主循環每步
UPDATE_HASH(ins_h, window[strstart + MIN_MATCH - 1]); // 納入第 3 字節
prev[strstart & WMASK] = head[ins_h]; // 入鏈
head[ins_h] = strstart; // 更新鏈頭
查找最長匹配:
match = head[ins_h]; // 最新候選
best_len = 0;
while (match != NIL && strstart - match <= MAX_DIST) {
// 比較 window[strstart...] 與 window[match...]
if (window[match + best_len] == window[strstart + best_len]) {
// 延伸匹配...
}
match = prev[match & WMASK]; // 遍歷鏈表
}
發現匹配後不立即輸出,而是嘗試 strstart+1 位置的匹配:
if (best_len >= MIN_MATCH) {
// 保存當前匹配
match_length = best_len;
match_start = match;
// 前瞻一步
strstart++; lookahead--;
UPDATE_HASH(ins_h, window[strstart + MIN_MATCH - 1]);
// 再次查找...
if (new_best_len > match_length) {
// 新匹配更長,輸出字面量 window[strstart-1],採用新匹配
} else {
// 原匹配更優,輸出 <長度,距離>
}
}
效果:避免「貪婪匹配」導致錯過更長匹配,提升壓縮率 ~2-3%。
dyn_ltree[], dyn_dtree[]。gen_bitlen 計算各符號編碼長度,gen_codes 生成標準 Huffman 碼 (按長度排序賦碼)。
// 位級寫入緩衝區
ulg bi_buf; // 32 位緩衝器
int bi_valid; // 有效位數
void send_bits(int code, int len) {
bi_buf |= (code << bi_valid);
bi_valid += len;
while (bi_valid >= 8) {
put_byte(bi_buf & 0xff);
bi_buf >>= 8;
bi_valid -= 8;
}
}
main()
→ treat_file() // 打開文件
→ zip() // 壓縮入口
→ bi_init() // 位流初始化
→ ct_init() // Huffman 樹初始化 (靜態/動態)
→ lm_init() // LZ77 初始化 (head 清零, strstart=0, 預讀 2 字節)
→ deflate() // 核心壓縮循環
→ longest_match() // 哈希鏈查找最長匹配
→ ct_tally() // 統計符號頻率 / 輸出字面量或
→ flush_block() // 塊滿時構建動態樹並輸出
→ bi_windup() // 刷新剩餘位
→ put_bytes() // 輸出 gzip 尾部 CRC-32 + 原始長度
| 技術 | 作用 | gzip 實現細節 |
|---|---|---|
| 滑動窗口 | 限制搜索範圍,O(1) 內存 | 32KB 循環緩衝區,WMASK=32767 |
| 哈希鏈 | O(1) 平均查找候選匹配 | 3 字節哈希,head/prev 雙數組 |
| 延遲匹配 | 避免局部最優 | 前瞻 1 步,比較兩輪匹配長度 |
| 動態 Huffman | 適應數據統計特性 | 每塊重建樹,樹結構本身再壓縮 |
| 懶惰評估 | 減少無效比較 | 匹配長度 ≥ GOOD_MATCH(32) 時直接採用 |
| 距離編碼 | 近距離用少位數 | 距離碼 0-29 對應 1-32768,指數增長 |
gzip-1.2.4.tar.gz,閱讀 deflate.c、trees.c、bits.c。deflate() 主循環設置斷點,觀察 strstart、match_length、bi_buf 變化。MIN_MATCH 為 2/4,觀察壓縮率與速度變化。zlib (deflate.c 同源) 的 compress()/uncompress() 封裝層。PNG 如何在 IDAT chunk 中使用 zlib 流 (濾波器 + Deflate)。Deflate 是「理論優雅、工程極致」的經典之作,深入其源碼是理解無損壓縮、流式編碼、位級操作的最佳教材。