zipline 压缩解压缩算原理简介
1. 相关专有名字和模型说明
1. 1. 核心模型
压缩解压缩(lz77算法)算法维护一个固定大小的滑动窗口,窗口分为两部分:
-
历史缓冲区(History Buffer):存放最近处理过的 N 字节数据,作为匹配查找的字典;
-
前瞻缓冲区(Look-ahead Buffer):存放待压缩的原始数据。
压缩时,从当前待压缩位置出发,在前瞻缓冲区中取连续字节,与历史缓冲区中所有位置的字节序列逐一比对,寻找最长的连续匹配串
1. 2. 专有名词

滑动窗口(Sliding Window)
算法维护一个固定长度的窗口,随着编码进度从左向右逐字节滑动,窗口总长度 = 查找缓冲区大小 + 先行缓冲区大小。
查找缓冲区(Search Buffer / Dictionary)
窗口左侧的灰色区域,也叫历史缓冲区 / 字典区,存放已经完成编码的历史数据。压缩时在这片区域内查找与待压缩数据匹配的字节序列,是匹配的 “字典来源”。
先行缓冲区(Look-ahead Buffer)
窗口右侧的黄色区域,也叫前瞻缓冲区,存放等待被压缩的原始数据。算法从该区的起始位置开始,在历史区中寻找最长的连续匹配串。
查找指针与核心参数
偏移量(Offset):匹配序列在历史缓冲区中的起始位置,距离当前编码点的字节距离(图中偏移量 = 2);
长度(Length):找到的连续匹配字节的数量;
若找不到满足最小长度的匹配,则输出当前字节作为字面量(Literal)。
2. LZ77 完整编码流程示例图

2.1 流程对应算法逻辑
1. 初始状态:滑动窗口内无历史数据,先行缓冲区的字符无法匹配,依次作为字面量输出(图中步骤 1-3);
2. 当历史缓冲区出现重复序列时,计算匹配的偏移量和长度,输出 (偏移量, 长度) 指针(如图中步骤 4 输出 (6,2,C),代表在偏移 6 的位置找到长度为 2 的匹配,后续紧跟字面量 C);
3. 每输出一组码字后,滑动窗口向右移动对应字节数,历史缓冲区淘汰最左侧的旧数据,纳入新的已编码数据,先行缓冲区补充后续原始数据;
4. 重复上述过程,直到所有数据处理完成。
_______________________________________________________________________________________________________________________________________________________________
LZ77 算法统一遵循的两级优先级匹配选择规则,执行顺序严格固定:
- 第一优先级:最长匹配长度优先
永远先以「连续匹配字节数最大」为第一目标,无论偏移量大小。
1. 只要某个匹配的长度更长,哪怕它偏移量很大、位置很远,也会被优先选中;
2. 核心原因:匹配长度直接决定了能替代多少个原始字节,是压缩收益的最主要来源,优先保证最长匹配,才能最大化压缩率。
- 第二优先级:长度平局时,取偏移量最小(最近命中)
当历史区中存在多个长度完全相同的最长匹配时,选择 offset 数值最小的那个 —— 也就是距离当前编码位置最近、历史上最晚出现的匹配串。
1. 核心原因:偏移量越小,后续霍夫曼编码的码字越短,编码开销越低,最终整体压缩收益更高;//霍夫曼编码的作用是负责节省bit 。
2. 对应你之前看到的 Project Zipline 硬件 spec 里的明确规则:ties go to the lower offset(平局偏向更低的偏移)。
即先对比length ,取length最高的,length一样的时候 选取offset数值最小的那个。
3. LZ77算法举例说明
首先设置匹配规则:
-
最小匹配长度 : 4 ;
-
采用最长匹配 ;
-
只在已经进入历史缓冲区的数据中查找 ;
-
暂不考虑 lazy matching、MTF 和 Huffman 编码 ;
-
<offset,length>是 LZ77 阶段的中间结果 ;
**整体流程如下: **

待压缩的数据为 a,b,a,b,a,b,c,a,b,c,a,b,a,b,c,d,a,b,c,a,b,c,a,b,c,c,a,b,c,c,a,b,c,c为例子进行说明:
最终的输出结果是 a,b,a,b,a,b,c,a,b,c<8,5><12,8><19,4>;
具体步骤如下:
_______________________________________________________
- 步骤1:
历史区 H : 空;
前瞻区 L: 【a】 b a b a b c a b c a b a b c d a b c a b c a b c c a b c c a b c c ;
输出 = a ;
_______________________________________________________
- 步骤2:
历史区 H : 【a】;
前瞻区 L : 【b】a b a b c a b c a b a b c d a b c a b c a b c c a b c c a b c c ;
输出 = a b ;
_______________________________________________________
- 步骤3:
历史区 H : 【ab】;
前瞻区 L : 【a】b a b c a b c a b a b c d a b c a b c a b c c a b c c a b c c ;
最加匹配 : ab ,长度2,小于4 ;
输出 = a b a ;
_______________________________________________________
- 步骤4:
历史区 H : 【a b a】;
*前瞻区 L : 【b】a b c a b c a b a b c d a b c a b c a b c c a b c c a b c c ;
最加匹配 : ba ,长度2,小于4 ;
输出 = a b a b ;
_______________________________________________________
- 步骤5:
历史区 H : 【a b a b】;
前瞻区 L : 【a】b c a b c a b a b c d a b c a b c a b c c a b c c a b c c ;
最加匹配 : ab ,长度2,小于4 ;
输出 = a b a b a ;
_______________________________________________________
- 步骤6:
历史区 H : 【a b a b a】;
前瞻区 L : 【b】c a b c a b a b c d a b c a b c a b c c a b c c a b c c ;
最加匹配 : b ,长度1,小于4 ;
输出 = a b a b a b ;
_______________________________________________________
- 步骤7:
历史区 H : 【a b a b a b】;
前瞻区 L : 【c】a b c a b a b c d a b c a b c a b c c a b c c a b c c ;
最加匹配 : b ,长度1,小于4 ;
输出 = a b a b a b c ;
_______________________________________________________
- 步骤8:
历史区 H : 【a b a b a b c】;
前瞻区 L : 【a】b c a b a b c d a b c a b c a b c c a b c c a b c c ;
最加匹配 : abc ,长度3,小于4 ;
输出 = a b a b a b c a ;
_______________________________________________________
- 步骤9:
历史区 H : 【a b a b a b c a】;
前瞻区 L : 【b】c a b a b c d a b c a b c a b c c a b c c a b c c ;
最加匹配 : bca ,长度3,小于4 ;
输出 = a b a b a b c a b ;
_______________________________________________________
- 步骤10:
历史区 H : 【a b a b a b c a b】;
前瞻区 L : 【c】a b a b c d a b c a b c a b c c a b c c a b c c ;
最加匹配 : cab ,长度3,小于4 ;
输出 = a b a b a b c a b c ;
_______________________________________________________
- 步骤11:
历史区 H : 【a b {a b a b c} a b c】;
------------------|3-------7|--------------
前瞻区 L : 【a b a b c】d a b c a b c a b c c a b c c a b c c ;
------------|11-----15|---------------------------------------
offset = 11 - 3 =8 ;
lenth = 5 ;
最加匹配 : ababc ,长度5,大于4 ;
输出 = a b a b a b c a b c <8,5> ;
_______________________________________________________
- 步骤12:
历史区 H : 【a b a b a b c a b c a b a b c】;
前瞻区 L : 【d】 a b c a b c a b c c a b c c a b c c ;
输出 = a b a b a b c a b c <8,5> d ;
_______________________________________________________
- 步骤13:
历史区 H : 【a b a b { a b c a b c a b } a b c d】;
---------------------- |5------------12|-------------
前瞻区 L : 【a b c a b c a b】c c a b c c a b c c ;
------------|17 ----------24|
offset = 17 - 5 =12 ;
lenth = a b c a b c a b ,长度8,大于4;
输出 = abababcabc<8,5> d <12,8> ;
_______________________________________________________
- 步骤14:
历史区 H : 【a b a b a b c a b c a b a b c d a b c a b c a b】
前瞻区 L : 【c】c a b c c a b c c ;
输出 = abababcabc<8,5>d <12,8> c ;
_______________________________________________________
- 步骤15:
查询步骤如下:
- 匹配查询1
|--------------------- 历史区 H----- ------------------------|---前瞻区域 L-----|
数据排列 a b a b a b { c a b c }a b a b c d a b c a b c a b c | c a b c c a b c c
----------------------|7----10 |--------------------------------------------------
前瞻区 L : 【c a b c 】 c a b c c ;
-----------|26-----29|---------------
故 : 历史区域匹配项 : 【c a b c】 ;
offset = 26 - 7 = 19 ,
lenth = 4 ;
__________________________________________________________________________________
- 匹配查询2
|--------------------- 历史区 H----- ---------------------|---前瞻区域 L-----|
数据排列 a b a b a b c a b c a b a b c d a b {c a b c }a b c | c a b c c a b c c
--------------------------------------------|19--22|---------------------------
前瞻区 L : 【c a b c 】 c a b c c ; -------
-----------|26-----29|-------------------------------
故 : 历史区域匹配项 : 【c a b c】 ;
offset = 26 - 19 = 7 ;
lenth = 4 ;
____________________________________________________________________________
- 匹配查询3
|--------------------- 历史区 H----- ------------------- |---前瞻区域 L-----|
数据排列 a b a b a b c a b c a b a b c d a b c a b {c a b c | c a b c c }a b c c
--------------------------------------------------|22----------------30|-------------
前瞻区 L : 【c a b c c a b c c 】 ;
-----------|26---------------34|--------
故 : 历史区域匹配项 : 【c a b c c a c b c c】 ;
offset= 26 - 19 = 7 ;
lenth = 9 ;
输出 = a b a b a b c a b c <8,5> d <12,8> c <4,9> ;
4. Huffman算法举例说明:
**4. 1. LZ7算法与Huffman 说明 **
LZ77 负责“把重复内容变成引用”,哈夫曼 负责“把这些引用再编码得更短”。两者解决的问题不一样,所以通常是串起来用;
LZ77 做完以后,输出并不是“已经最省”的比特流,而是这两类符号:
- 字面量 literal:原样输出的字节
- 匹配对 length, distance:表示“从前面某个位置拷贝多长”
比如一段数据经过 LZ77 后,可能变成: - a
- b
- <offset=6, len=4>
- c
- <offset=3, len=5>
这些符号本身如果直接定长存储,还是会浪费很多 bit。
这时候哈夫曼的作用就是: - 统计哪些符号出现频繁
- 给高频符号分配更短的码字
- 给低频符号分配更长的码字
这样整体比特数继续下降。
可以把它理解成两步:
- LZ77 去掉“长距离重复”
- Huffman 去掉“符号分布不均匀”带来的冗余
所以哈夫曼加入后的作用是:
- 进一步提升压缩率
- 尤其对 literal / length / distance 这类分布很不均匀的符号效果明显
- 使输出符合 Deflate / zlib / gzip 这类标准格式
**4. 2. Huffman 算法 说明 **
对于压缩解压缩算法中,哈夫曼算法主要分为两种:fixed Huffman/dynamic Huffman它们都是 Deflate 里的两种 Huffman 编码方式。
区别很简单:
- fixed/static Huffman
- 码表是标准里直接规定好的
- 压缩时不用先统计本块数据频率
- 拿到 LZ77 输出的 literal/length/distance 后就可以直接编码
- 实现简单,硬件也更容易做
- 但压缩率通常不是最优
- dynamic Huffman
- 码表不是固定的
- 需要先统计当前
block里各种symbol出现频率 - 再根据频率生成** Huffman 树**
- 然后把“码表信息 + 编码后的数据”一起输出
- 压缩率通常更高,但实现更复杂
等整个文件全部输出后”,而是通常按 block 来看。
4. 3. fixed huffman & dynamic huffman 处理过程
- 对 固定 Huffman:
- LZ77 一边吐 token
- Huffman 一边直接编码
- 不需要等完整 block 结束才知道码表
- 对 动态 Huffman:
- 至少要先拿到 一个 block 内的 token 统计结果
- 然后生成这个 block 的 Huffman 表
- 再输出:
- 动态表头
- 用该表编码后的 token 流
所以动态模式通常是:
- LZ77 先产生一个 block 的 token
- 同时统计这个 block 里每个 symbol 出现次数
- block 结束后,生成 literal/length tree 和 distance tree
- 先把树描述写出去
- 再把刚才缓存的 token 用新码表编码输出
所以
- 静态:不用等,LZ77 token 出来就能编码
- 动态:通常要等一个 block 的 token 统计完成,不需要等整个文件
4. 4. dynamic huffman stream 处理流程与注意事项

这张图里最关键的点是:
- stream 是整个压缩流
- block 是 stream 里面的一段
- dynamic Huffman table 只对“当前 block”生效
- EOB=256 是当前 block 的结束标志
- 下一个 block 可以重新换一套表
所以 dynamic Huffman 的表作用范围不是“整个文件固定一套”,而是: - 通常一块一套
- 每个 block 都可以重新统计、重新建表
再看 block 内部:
![image]()
也就是说 dynamic block 的内容顺序是:
- block 头
- Huffman 表描述
- 用这张表编码的数据
- EOB
所以动态表不是单独存在的,它是“跟着这个 block 一起发出去”的。
然后再说你最容易混的 32KB window。
![image]()
这里要分清两件事:
- LZ77 window 限制的是:
- 当前 match 的 distance 最多能回看多远
- block 限制的是:
- 一张 Huffman 表覆盖多长的数据段
所以:
- 一张 Huffman 表覆盖多长的数据段
- 32KB 是回看窗口限制
- 不是 block 最大长度限制
举个例子:
假设一个 dynamic block 有 200KB 原始数据,这在标准上是可以的。
但这个 block 里面任意一个 <length,distance>: - distance 仍然不能超过 32KB
- length 仍然按 Deflate length code 规则来
也就是: - block 很长,合法
- 单次回拷距离仍受 32KB 限制
你可以把它想成这样: - Huffman 表解决“这一大段数据里的 token 怎么编码更省”
- LZ77 window 解决“某个 match 能从前面多远的位置复制”
边界关系图:
![image]()
所以结论可以压成 4 句: - dynamic Huffman 表的作用域是“一个 block”
- block 的结束靠 EOB=256
- block 大小标准上没有固定字节上限
- 32KB 限制的是 LZ77 回看距离,不是 block 大小
4. 5. dynamic huffman 处理流程举例说明
整个 dynamic block 的流程可以画成这样:

理解成:
LZ77 先把“原文”变成“符号流”,然后 dynamic Huffman 再根据这批符号流现算一套最合适的码表。
可以用上面lz77算法中的结果abababcabc<8,5>d<12,8>c<4,9> 进行动态huffman编码,下面是详细的步骤:
————————————————————————————————————————————————————————————————————————————————————————————————————————
第 1 步:LZ77 输出 token 流
你的 token 流是:
- a
- b
- a
- b
- a
- b
- c
- a
- b
- c
- <8,5>
- d
- <12,8>
- c
- <4,9>
转成 Deflate 语义后,不是把 <8,5> 当一个整体,而是拆成:
literal/length流distance流
所以得到:
literal/length 流:
- 97(a)
- 98(b)
- 97(a)
- 98(b)
- 97(a)
- 98(b)
- 99(c)
- 97(a)
- 98(b)
- 99(c)
- 259(len=5)
- 100(d)
- 262(len=8)
- 99(c)
- 263(len=9)
- 256(EOB)
distance 流:
- 5 对应 dist=8
- 6 对应 dist=12
- 3 对应 dist=4
#########################################################
补充说明:
1. a -> 97
- 这不是 Deflate 另外发明的一张表
- 而是字符 a 的字节值本来就是 ASCII 97,也就是 0x61
- Deflate 规定:普通字面量字节直接用 0~255 这个 literal 范围表示
- 所以字节 a 进入 Deflate 后,它的 literal symbol 就是 97;
2. 256(EOB)、259(len=5)、262(len=8)、263(len=9)
- 这些才是 RFC 1951 明确定义的符号编号
- 也就是标准协议规定好的 literal/length alphabet 里的编码含义
3. 0~29 的 distance code- 是距离符号 distance codes
所以:
- a -> 97,本质上是“字节值直接拿来当 literal symbol”
- len=5 -> 259,这个才是 RFC 1951 的长度码映射规则
- dist=8 -> code 5,这个也是 RFC 1951 的距离码映射规则
############################################################
————————————————————————————————————————————————————————————————————————————————————————————————————————
第 2 步:统计当前 block 的频次
dynamic Huffman 不是看到一个 symbol 就立刻编码,而是要先统计这个 block 里每个 symbol 出现了多少次。
本例中:
literal/length 频次:
- 97(a):4
- 98(b):4
- 99(c):3
- 100(d):1
- 259(len=5):1
- 262(len=8):1
- 263(len=9):1
- 256(EOB):1
distance 频次:
- 3:1
- 5:1
- 6:1
这一步的意思就是:
“当前 block 里,哪些符号最常见,哪些最少见?”
————————————————————————————————————————————————————————————————————————————————————————————————————————
第 3 步:生成两棵 Huffman 树
Deflate dynamic block 里始终是两棵树:
- 一棵给 literal/length
- 一棵给 distance
因为本例里: - a、b 最多
- c 次之
- d、几个 length 符号、EOB 最少
所以生成出来的树一定满足这个方向: - 97(a)、98(b) 码更短
- 99(c) 次短
- 100、259、262、263、256 会更长
distance 侧因为只有 3/5/6 三个符号,树会很小。
注意一点:
dynamic Huffman 真正先生成的通常不是“最终 bit 串”,而是先得到每个 symbol 的 码长,再按 canonical Huffman 规则确定具体码字。
也就是说流程是:
- 先算每个 symbol 的频次
- 决定每个 symbol 的 code length
- 再由 code length 推导具体码字
————————————————————————————————————————————————————————————————————————————————————————————————————————
第 4 步:输出 dynamic block 的头
dynamic block 的头不是只有 BFINAL/BTYPE,还要带“这两棵树长什么样”的信息。
顺序是:
- BFINAL
- BTYPE = 10,表示 dynamic Huffman
- HLIT
- HDIST
- HCLEN
- code-length alphabet 的码长
- literal/length tree 的码长描述
- distance tree 的码长描述
这一步的本质是:
解压器必须先知道“你这一个 block 用的是哪张 Huffman 表”,它才能解后面的数据。
所以 dynamic block 会比 fixed block 多一段“表描述开销”。
————————————————————————————————————————————————————————————————————————————————————————————————————————
第 5 步:再编码 token 数据
等两棵树描述完以后,才开始真正编码 token 数据。
也就是把:
- 97
- 98
- 97
- 98
- ...
- 259
- 100
- 262
- 99
- 263
- 256
按 literal/length 树编码。
同时,遇到长度符号时,再跟着输出对应的: - distance Huffman code
- extra bits
例如: - len=5, dist=8
- len=5 -> 259
- dist=8 -> dist code 5 + extra bit
- len=8, dist=12
- len=8 -> 262
- dist=12 -> dist code 6 + extra 2 bits
- len=9, dist=4
- len=9 -> 263
- dist=4 -> dist code 3
所以真正编码顺序是:
- 先发
literal/length symbol - 如果它是
length symbol,再接着发distance symbol和extra bits - 最后发 EOB=256
————————————————————————————————————————————————————————————————————————————————————————————————————————
第 6 步:输出 EOB,结束当前 block
256 是 End Of Block。
它表示:
- 当前 block 的 token 数据到这里结束
- 后面如果还有数据,可以开下一个 block
- 下一个 block 可以继续用 dynamic,也可以换成 fixed,甚至 uncompressed
所以 dynamic Huffman 表的作用范围,正好就是: - 从当前 block 头开始
- 到 EOB 结束为止
————————————————————————————————————————————————————————————————————————————————————————————————————————
4. 6.dynamic Huffman树的建立
当有很多频次相同的symbol的时候,所以动态哈夫曼表的建立并不是唯一的;所以下面给出的例子展示是一组最优的合法的结果;
如果编码器再同频时的tie-break 规则不同,树形可能不一样,但是码长分布和压缩效果通常等价。
Deflate 的 dynamic Huffman 整条链路串成一张结构图,再把每一步“作用 / 输入 / 输出
先固定例子:
- 原文:abababcabcababcdabcabcabccabccabcc
- 你前面给出的 LZ77 输出:a b a b a b c a b c <8,5> d <12,8> c <4,9>
这里 <8,5> 仍按: - distance/offset = 8
- length = 5
前半段:先把“原文”变成“token 和码表”
后半段:再把“码表 + token”编码成真正的 Deflate 比特流
流程图下:
![image]()
步骤如下:
Step 0****:原始输入
作用:
- 输入待压缩原文
输入: - abababcabcababcdabcabcabccabccabcc
输出长相:
原始字节流:
a b a b a b c a b c a b a b c d a b c a b c a b c c a b c c a b c c
这一步还没有 Huffman,也还没有 Deflate symbol。
————————————————————————————————————————————————————————————————————————————————————————————————————————
Step 1:LZ77 匹配
作用:
- 找历史窗口里的最长匹配
- 输出两种东西:
literalmatch(distance,length)
输入:
- 原始字节流
输出长相:
LZ77 输出:
a b a b a b c a b c <8,5> d <12,8> c <4,9>
含义:
- 前 10 个字符先作为 literal
- 然后找到一个匹配 <8,5>
- 再输出 d
- 再找到 <12,8>
- 再输出 c
- 再找到 <4,9>
注意: - 到这里为止,还只是 LZ77 语义
- 还没变成 RFC 1951 里的 symbol
————————————————————————————————————————————————————————————————————————————————————————————————————————
Step 2:Deflate token 化
作用:
- 把 LZ77 结果转换成 Deflate 规定的两路符号:
- literal/length
- distance
输入:
- a b a b a b c a b c <8,5> d <12,8> c <4,9>
输出长相:
literal/length 流:
97 98 97 98 97 98 99 97 98 99 259 100 262 99 263 256
distance 流:
5 6 3
解释:
- a -> 97
- b -> 98
- c -> 99
- d -> 100
- EOB -> 256
- len=5 -> 259
- len=8 -> 262
- len=9 -> 263
距离侧:
- dist=8 -> dist symbol 5
- dist=12 -> dist symbol 6
- dist=4 -> dist symbol 3
所以这一步之后,数据已经进入 RFC 1951 的语言体系了。
————————————————————————————————————————————————————————————————————————————————————————————————————————
################################# begin ##############################################################
查表补充说明:
所以对任意匹配 <distance, length>:
- 先把 length 查成:- length code
- length extra bits
- 再把 distance 查成:- distance code
- distance extra bits
- 最终输出顺序是:- length code 的 Huffman 码
- length extra bits
- distance code 的 Huffman 码
- distance extra bits
- 两个特别容易记错的点
- 257~285 是
length code,不是“长度值本身” - 0~29 是
distance code,不是“距离值本身”
也就是说: - 285 不表示长度 285,它表示长度 258
- 29 不表示距离 29,它表示距离区间 24577~32768;
0~29指的是30个distance 符号编号,每个 distance code 是一个“符号编号”,真正输出到压缩流里时,还要结合:
- Huffman code
- extra bits
所以要分三层看:
****distance symbol/code- 例如 0,1,2,...,29 - 这是“编号”
Huffman code- 这是这个编号经过 fixed/dynamic Huffman 后得到的变长码字
extra bits- 用来补充具体距离值
比如在 Deflate 里:
- distance code 0 表示距离 1
- distance code 1 表示距离 2
- distance code 2 表示距离 3
- distance code 3 表示距离 4
- distance code 4 表示距离 5~6
- distance code 5 表示距离 7~8
- ...
- distance code 29 表示距离 24577~32768
所以像你前面举的:
- distance = 8
并不是直接把数字 8 原样写进去,而是先查表: - 8 落在 distance code 5 这个区间里
- code 5 的 base distance 是 7
- 所以 extra bits = 8 - 7 = 1
也就是最终表示成: - distance symbol = 5
- extra bits = 1
另:distance 和lenth 映射表:
[https://www.cnblogs.com/hulierduo-fox/p/22889905](压缩解压缩之distance 0~29全映射表);
[https://www.cnblogs.com/hulierduo-fox/p/22889834](压缩解压缩之lenth code 257~285映射表)
################################# end ###############################################
Step 3:统计频次
作用:
- 为当前 block 里的 symbol 建动态 Huffman 表
- 统计的是“当前 block”的频次,不是整个文件永久一张表
输入: - literal/length 流
- distance 流
输出长相:
LL 频次:
97(a) : 4
98(b) : 4
99(c) : 3
100(d) : 1
259(len=5) : 1
262(len=8) : 1
263(len=9) : 1
256(EOB) : 1
Dist 频次:
3 : 1
5 : 1
6 : 1
这一步的直觉就是:
- 高频符号以后应该拿短码
- 低频符号以后应该拿长码
————————————————————————————————————————————————————————————————————————————————————————————————————————
Step 4:生成 Huffman 码长
作用:
- 根据频次先求每个 symbol 的 code length
- 在 Deflate dynamic block 里,真正关键的是“码长表”
输入: - 频次统计结果
输出长相:
-输出码长code lenth 是二叉树中子节点到root的距离;
LL code length:
97 -> 2
98 -> 2
99 -> 3
263 -> 3
100 -> 4
256 -> 4
259 -> 4
262 -> 4
Dist code length:
3 -> 1
5 -> 2
6 -> 2
###################### begin ###############################################################
详解:
1:literal/lenth 树
deflate code 的统计结果是
literal/length
- 97(a):4
- 98(b):4
- 99(c):3
- 100(d):1
- 256(EOB):1
- 259(len=5):1
- 262(len=8):1
- 263(len=9):1
首先是将统计结果中频次最少的进行合并结合,因为有多个相同的频次,所以合并结合的结果并不唯一;下面是其中一种合并结果; - (100,256) -> N1=2
- (259,262) -> N2=2
- (263,N1) -> N3=3
- (N2,99) -> N4=5
- (N3,97) -> N5=7
- (98,N4) -> N6=9
- (N5,N6) -> ROOT=16
![image]()
-输出码长code lenth是二叉树中子节点到root的距离;
所以由这棵树得到的码长code lenth是: - 97(a):2
- 98(b):2
- 99(c):3
- 263(len=9):3
- 100(d):4
- 256(EOB):4
- 259(len=5):4
- 262(len=8):4
同理:
2.distance 侧更简单: - (5, 6) -> D1 = 2
- (3, D1) -> ROOT = 3
![image]()
得到码长code lenth:
- 3:1
- 5:2
- 6:2
可以把它理解成一棵“合法、最优”的 Huffman 树对应出来的深度。
这里有个关键点:
- 同频时树形不唯一
- 但最后只要码长集合合法,就能继续做 canonical Huffman
所以 dynamic Huffman 更准确地说不是“先固定一棵唯一树”,而是: - 先得到每个 symbol 的码长
注意一点:
dynamic Huffman 真正先生成的通常不是“最终 bit 串”,而是先得到每个 symbol 的 码长,再按 canonical Huffman 规则确定具体码字。
_______________________________________________________________________________________________________________________________
Step 5:Canonical Huffman
作用:
- 把“码长表”变成“最终可编码的具体码字”
- 这样不用传整棵树,只传码长就能重建
上述的得到的huffman code lenth 还需要按照canonical Huffman 规则确定具体码字;
canonical Huffman分配规则可以简化理解成:
- 先按 code length 从短到长排序;
- 同长度内按 symbol 数值从小到大排序;
- 从最小二进制值开始顺序分配;
所以它的好处是:
- 表示简单;
- 解码器实现方便;
- 同一组码长能唯一恢复出码字;
所以整个的canonical huffman 的最终码字的输出如下:
输入:
上一步的 code length;
输出长相:
1.LL canonical code:
97(a) -> 00
98(b) -> 01
99(c) -> 100
263(len=9) -> 101
100(d) -> 1100
256(EOB) -> 1101
259(len=5) -> 1110
262(len=8) -> 1111
2.Dist canonical code:
3(dist=4) -> 0
5(dist=8) -> 10
6(dist=12) -> 11
############ begin ##############################################
详解
先看 literal/length。
按 (码长, symbol) 排序后是:
- 长度 2:97, 98
- 长度 3:99, 263
- 长度 4:100, 256, 259, 262
按 canonical 规则分配后:
| symbol | 含义 | code length | canonical code |
|---|---|---|---|
| 97 | a |
2 | 00 |
| 98 | b |
2 | 01 |
| 99 | c |
3 | 100 |
| 263 | len=9 |
3 | 101 |
| 100 | d |
4 | 1100 |
| 256 | EOB |
4 | 1101 |
| 259 | len=5 |
4 | 1110 |
| 262 | len=8 |
4 | 1111 |
distance 侧,排序后:
- 长度 1:3
- 长度 2:5, 6
按 canonical 规则分配后:
| dist symbol | 含义 | code length | canonical code |
|---|---|---|---|
| 3 | dist=4 |
1 | 0 |
| 5 | dist=8 |
2 | 10 |
| 6 | dist=12 |
2 | 11 |
############ end ##############################################
————————————————————————————————————————————————————————————————————————————————————————————————————————
Step 6:生成 dynamic block 头
作用:
- 告诉解压器:当前 block 用的是 dynamic Huffman
- 并把“这张表长什么样”描述出去
输入:
- LL code length
- Dist code length
输出长相,不是正文数据,而是“表描述”:
Block Header:
BFINAL = 1/0
BTYPE = 10 // dynamic Huffman
随后输出:
HLIT
HDIST
HCLEN
code-length alphabet 的码长
LL 树码长描述
Dist 树码长描述
这一步要特别理解:
- 真正输出的不是“画出来的树”
- 也不是直接输出 97->00
- 而是输出“码长信息”
在 RFC 1951 里,LL 和 Dist 的码长序列还会继续做一层压缩,用 16/17/18 这些 code-length repeat symbol 表示重复的码长和长串的 0。
所以这一步的输出“长相”更像:
dynamic block 头 + code-length tree + LL/Dist 的码长序列描述






浙公网安备 33010602011771号