zipline 压缩解压缩算原理简介

1. 相关专有名字和模型说明

1. 1. 核心模型

压缩解压缩(lz77算法)算法维护一个固定大小的滑动窗口,窗口分为两部分:

  • 历史缓冲区(History Buffer):存放最近处理过的 N 字节数据,作为匹配查找的字典;

  • 前瞻缓冲区(Look-ahead Buffer):存放待压缩的原始数据。

压缩时,从当前待压缩位置出发,在前瞻缓冲区中取连续字节,与历史缓冲区中所有位置的字节序列逐一比对,寻找最长的连续匹配串

1. 2. 专有名词

image

滑动窗口(Sliding Window)

算法维护一个固定长度的窗口,随着编码进度从左向右逐字节滑动,窗口总长度 = 查找缓冲区大小 + 先行缓冲区大小。

查找缓冲区(Search Buffer / Dictionary)

窗口左侧的灰色区域,也叫历史缓冲区 / 字典区,存放已经完成编码的历史数据。压缩时在这片区域内查找与待压缩数据匹配的字节序列,是匹配的 “字典来源”。

先行缓冲区(Look-ahead Buffer)

窗口右侧的黄色区域,也叫前瞻缓冲区,存放等待被压缩的原始数据。算法从该区的起始位置开始,在历史区中寻找最长的连续匹配串。

查找指针与核心参数

偏移量(Offset):匹配序列在历史缓冲区中的起始位置,距离当前编码点的字节距离(图中偏移量 = 2);

长度(Length):找到的连续匹配字节的数量;

若找不到满足最小长度的匹配,则输出当前字节作为字面量(Literal)。

2. LZ77 完整编码流程示例图

image

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 阶段的中间结果 ;

**整体流程如下: **
image

待压缩的数据为 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。
    这时候哈夫曼的作用就是:
  • 统计哪些符号出现频繁
  • 给高频符号分配更短的码字
  • 给低频符号分配更长的码字
    这样整体比特数继续下降。
    可以把它理解成两步:
  1. LZ77 去掉“长距离重复”
  2. 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 表
    • 再输出:
      1. 动态表头
      2. 用该表编码后的 token 流
        所以动态模式通常是:
  1. LZ77 先产生一个 block 的 token
  2. 同时统计这个 block 里每个 symbol 出现次数
  3. block 结束后,生成 literal/length tree 和 distance tree
  4. 先把树描述写出去
  5. 再把刚才缓存的 token 用新码表编码输出
    所以
  • 静态:不用等,LZ77 token 出来就能编码
  • 动态:通常要等一个 block 的 token 统计完成,不需要等整个文件

4. 4. dynamic huffman stream 处理流程与注意事项

image
这张图里最关键的点是:

  • stream 是整个压缩流
  • block 是 stream 里面的一段
  • dynamic Huffman table 只对“当前 block”生效
  • EOB=256 是当前 block 的结束标志
  • 下一个 block 可以重新换一套表
    所以 dynamic Huffman 的表作用范围不是“整个文件固定一套”,而是:
  • 通常一块一套
  • 每个 block 都可以重新统计、重新建表
    再看 block 内部:
    image
    也就是说 dynamic block 的内容顺序是:
  1. block 头
  2. Huffman 表描述
  3. 用这张表编码的数据
  4. EOB
    所以动态表不是单独存在的,它是“跟着这个 block 一起发出去”的。
    然后再说你最容易混的 32KB window。
    image
    这里要分清两件事:
  • LZ77 window 限制的是:
    • 当前 match 的 distance 最多能回看多远
  • block 限制的是:
    • 一张 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 的流程可以画成这样:
image
理解成:
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 规则确定具体码字。
    也就是说流程是:
  1. 先算每个 symbol 的频次
  2. 决定每个 symbol 的 code length
  3. 再由 code length 推导具体码字
    ————————————————————————————————————————————————————————————————————————————————————————————————————————

第 4 步:输出 dynamic block 的头
dynamic block 的头不是只有 BFINAL/BTYPE,还要带“这两棵树长什么样”的信息。
顺序是:

  1. BFINAL
  2. BTYPE = 10,表示 dynamic Huffman
  3. HLIT
  4. HDIST
  5. HCLEN
  6. code-length alphabet 的码长
  7. literal/length tree 的码长描述
  8. 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 symbolextra 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 匹配
作用:

  • 找历史窗口里的最长匹配
  • 输出两种东西:
    • literal
    • match(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>:

  1. 先把 length 查成:- length code
  • length extra bits
  1. 再把 distance 查成:- distance code
  • distance extra bits
  1. 最终输出顺序是:- length code 的 Huffman 码
  • length extra bits
  • distance code 的 Huffman 码
  • distance extra bits
  1. 两个特别容易记错的点
  • 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分配规则可以简化理解成:

  1. 先按 code length 从短到长排序;
  2. 同长度内按 symbol 数值从小到大排序;
  3. 从最小二进制值开始顺序分配;
    所以它的好处是:
  • 表示简单;
  • 解码器实现方便;
  • 同一组码长能唯一恢复出码字;

所以整个的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 的码长序列描述

posted on 2026-09-04 14:36  狐狸耳朵FOX  阅读(9)  评论(0)    收藏  举报

导航