本文为个人学习笔记
如果觉得写的不错欢迎点赞收藏交流
如有疑问和建议欢迎留言或者私信
1. 霍夫曼编码 (The Huffman Code)
1.1 怎么定义好的编码?
由于计算机处理二进制位序列,人们需要一种编码模式将文本处理成二进制位的长串,以英文为例,26个字母,空格和5种标点符号共32个符号需要编码,以二进制表示则需要5位编码(
2
5
=
32
2^5 = 32
25=32),比如00000代表字母a,00001代表字母b,11111代表“…”。每个符号的五位都是完全充分的,平均每一个符号需要用到5位编码,ASCII就是利用这种方式的编码模式(当然,不止32位)。
数据压缩领域的一个基础问题便是“如何减少每个字母的平均位数”。我们注意到在实际的应用中,有些符号和字母比如a,b,c,d的出现频率明显高于x,y这些符号,用同样的位数传递低频词是一种浪费,所以也许我们可以对高频符号使用更小的位数,以达到“减少字母平均位数”的目的。
可变长编码模式:莫尔斯码
莫尔斯码把每个字母转换成点和划序列,分别对应0,1。对于频繁使用的字母使用短的位串进行编码,比如e 对应0,t 对应1,a对应01。
当然实际上,由于每个字母的位数不定,编码变得不确定,0既可以是字母e的编码,也可以是字母a的前缀码。所以实际上,莫尔斯码使用的是0,1,暂停 对字母进行编码。
前缀码: 解决前缀问题
设定映射函数
γ
\gamma
γ,对于不同的
x
,
y
∈
S
x,y \in S
x,y∈S,
γ
(
x
)
\gamma(x)
γ(x)不是
γ
(
y
)
\gamma(y)
γ(y)的前缀(就是映射表中不存在谁是谁的前缀)。
例:
字母集合
S
=
{
a
,
b
,
c
,
d
,
e
}
S=\{a,b,c,d,e\}
S={a,b,c,d,e}, 映射函数
γ
(
a
)
=
11
,
γ
(
b
)
=
01
,
γ
(
c
)
=
001
,
γ
(
d
)
=
10
,
γ
(
e
)
=
000
\gamma(a) = 11, \gamma(b) = 01, \gamma(c) = 001, \gamma(d) = 10, \gamma(e) = 000
γ(a)=11,γ(b)=01,γ(c)=001,γ(d)=10,γ(e)=000, 串 cecab 被编码为0010000011101,从左往右解码:
- 0,00都不是字母, 001是c,解码第一个001为字母c,这是一个安全的决定(不存在谁是谁的前缀)
- 删掉001,继续读0000011101,重复步骤1
最优前缀码
对于每个字母
x
∈
S
x \in S
x∈S,总计存在n个字母, 存在频率
f
x
f_x
fx,使得
∑
x
∈
S
f
x
=
1
\sum _{x\in S} f_x = 1
∑x∈Sfx=1。
我们有每个字母的平均二进制位数:
A
B
L
(
γ
)
=
∑
x
∈
S
f
x
∣
γ
(
x
)
∣
ABL(\gamma) = \sum_{x \in S} f_x |\gamma(x)|
ABL(γ)=x∈S∑fx∣γ(x)∣
很显然,达到ABL最小值的编码模式便是最优前缀码。
1.2 设计寻找最优前缀码的算法
蛮力搜索所有的可能的前缀码寻找到最优的前缀码实际上不太可行,所以我们选择描述一个贪心算法,可以有效地构造最优前缀码。
假设有一套前缀码
γ
\gamma
γ,我们用二叉树T表示它,树叶数等于字母表S的大小,叶子代表S中的字母。以下的定理和命题就不证明了…
定理1: 从T构成的S的编码是一个前缀码
定理2: 与最优前缀码对应的二叉树T是满的
问题变成对于所有叶子x(也是所有字母):
A
B
L
(
T
)
=
∑
x
∈
S
f
x
×
d
e
p
t
h
T
(
x
)
ABL(T) = \sum_{x \in S}f_x\times depth_T(x)
ABL(T)=x∈S∑fx×depthT(x)
我们现在假设有一颗树 T ∗ T^* T∗,对应最优前缀码的结构,但是没有标记树叶。我们有如下定理:
定理4: 存在一个与树 T ∗ T^* T∗对应的最优前缀码,其中最低频率的两个字母被指定为树叶,这两片树叶是兄弟。
所以假设最低频率的两个字母为y’, z’, 定理4告诉我们要将他们两作为同一父节点下的兄弟树叶,这个父亲节点是元字母x’,频率为y’, z’之和,此时直接提出算法(Huffman算法):
- 构建元字母x’, f x = f y + f z f_x = f_y + f_z fx=fy+fz,删除y’, z’,构建新的字母表S‘
- 对树T‘构建关于S‘的前缀码
γ
′
\gamma '
γ′。
从T’开始,拿掉标记为x’的树叶并在下面加上y’,x’的孩子
范例:
S
=
(
a
,
b
,
c
,
d
,
e
)
S = (a,b,c,d,e)
S=(a,b,c,d,e) 且频率
f
=
(
0.32
,
0.25
,
0.20
,
0.18
,
0.05
)
f = (0.32, 0.25, 0.20, 0.18, 0.05)
f=(0.32,0.25,0.20,0.18,0.05)
- 频率最低两个数字 d, e,合并成新的元字母 < d , e > <d,e> <d,e>,构建新的字母表 S ′ = ( a , b , c , < d , e > ) S' = ( a, b, c, <d,e>) S′=(a,b,c,<d,e>)对应频率 f ′ = ( 0.32 , 0.25 , 0.20 , 0.23 ) f' = (0.32, 0.25, 0.20, 0.23) f′=(0.32,0.25,0.20,0.23)
- 频率最低两个数字 c, <d,e>,合并成新的元字母
<
c
,
d
,
e
>
<c,d,e>
<c,d,e>,构建新的字母表
S
′
′
=
(
a
,
b
,
<
c
,
d
,
e
>
)
S'' = ( a, b, <c,d,e>)
S′′=(a,b,<c,d,e>)对应频率
f
′
′
=
(
0.32
,
0.25
,
0.43
)
f'' = (0.32, 0.25, 0.43)
f′′=(0.32,0.25,0.43)
…
Huffman算法下的贪心规则是:合并最小频率的字母,我们并不知道这个规则为什么与整体的算法相适合,我们能做的只是保证他们是兄弟节点,这就足以产生一个少了一个字母的等价的新问题。
最优性的证明: 不想写了
运行时间: 除了最后一次,每次迭代只: 识别最低频率的两个字母并组合成新字母,假设字母表有n个字母,对n-1此迭代求和则是 O ( n 2 ) O(n^2) O(n2)。但是实际上Huffman是使用优先队列的理想环境,我们在堆实现的优先队列上维护字母表S,是的每次插入和最小元素的取出用 O ( l o g n ) O(logn) O(logn)时间,对于所有n次迭代,共有 O ( n l o g n ) O(nlogn) O(nlogn)时间