1、香农编码
这个例子展示了一组字母的香浓编码结构(如图a所示)这五个可被编码的字母有如下出现次数:
Symbol | A | B | C | D | E |
Count | 15 | 7 | 6 | 6 | 5 |
Probabilities | 0.38461538 | 0.17948718 | 0.15384615 | 0.15384615 | 0.12820513 |
从左到右,所有的符号以它们出现的次数划分。在字母B与C之间划定分割线,得到了左右两组,总次数分别为22,17。 这样就把两组的差别降到最小。通过这样的分割, A与B同时拥有了一个以0为开头的码字, C,D,E的码子则为1,如图b所示。 随后, 在树的左半边,于A,B间建立新的分割线,这样A就成为了码字为00的叶子节点,B的码子01。经过四次分割, 得到了一个树形编码。 如下表所示,在最终得到的树中, 拥有最大频率的符号被两位编码, 其他两个频率较低的符号被三位编码。
符号 | A | B | C | D | E |
编码 | 00 | 01 | 10 | 110 | 111 |
2、哈夫曼编码
3、算术编码(理解即可)
算术编码的基本原理是将编码的消息表示成实数0和1之间的一个间隔(Interval),消息越长,编码表示它的间隔就越小,表示这一间隔所需的二进制位就越多。
算术编码用到两个基本的参数:符号的概率和它的编码间隔。信源符号的概率决定压缩编码的效率,也决定编码过程中信源符号的间隔,而这些间隔包含在0到1之间。编码过程中的间隔决定了符号压缩后的输出。
给定事件序列的算术编码步骤如下:
(1)编码器在开始时将“当前间隔” [ L, H) 设置为[0,1)。
(2)对每一事件,编码器按步骤(a)和(b)进行处理
(a)编码器将“当前间隔”分为子间隔,每一个事件一个。
(b)一个子间隔的大小与下一个将出现的事件的概率成比例,编码器选择子间隔对应于下一个确切发生的事件相对应,并使它成为新的“当前间隔”。
(3)最后输出的“当前间隔”的下边界就是该给定事件序列的算术编码。
例1:假设信源符号为{A, B, C, D},这些符号的概率分别为{ 0.1, 0.4, 0.2,0.3 },根据这些概率可把间隔[0, 1]分成4个子间隔:[0, 0.1], [0.1, 0.5], [0.5, 0.7], [0.7, 1],其中[x,y]表示半开放间隔,即包含x不包含y。上面的信息可综合在表03-04-1中。
表03-04-1 信源符号,概率和初始编码间隔
符号 | A | B | C | D |
概率 | 0.1 | 0.4 | 0.2 | 0.3 |
初始编码间隔 | [0, 0.1) | [0.1, 0.5) | [0.5, 0.7) | [0.7, 1] |
如果二进制消息序列的输入为:C A D A C D B。编码时首先输入的符号是C,找到它的编码范围是[0.5,0.7]。由于消息中第二个符号A的编码范围是[0, 0.1],因此它的间隔就取[0.5, 0.7]的第一个十分之一作为新间隔[0.5,0.52]。依此类推,编码第3个符号D时取新间隔为[0.514, 0.52],编码第4个符号A时,取新间隔为[0.514, 0.5146],…。消息的编码输出可以是最后一个间隔中的任意数。整个编码过程如图03-04-1所示。
图03-04-1 算术编码过程举例
这个例子的编码和译码的全过程分别表示在表03-04-2和表03-04-3中。
表03-04-2 编码过程
步骤 | 输入符号 | 编码间隔 | 编码判决 |
1 | C | [0.5, 0.7] | 符号的间隔范围[0.5, 0.7] |
2 | A | [0.5, 0.52] | [0.5, 0.7]间隔的第一个1/10 |
3 | D | [0.514, 0.52] | [0.5, 0.52]间隔的最后一个1/10 |
4 | A | [0.514, 0.5146] | [0.514, 0.52]间隔的第一个1/10 |
5 | C | [0.5143,0.51442] | [0.514, 0.5146]间隔的第五个1/10开始,二个1/10 |
6 | D | [0.514384,0.51442] | [0.5143, 0.51442]间隔的最后3个1/10 |
7 | B | [0.5143836,0.514402] | [0.514384,0.51442]间隔的4个1/10,从第1个1/10开始 |
8 | 从[0.5143876, 0.514402]中选择一个数作为输出:0.5143876 |
表03-04-3 译码过程
步骤 | 间隔 | 译码符号 | 译码判决 |
1 | [0.5, 0.7] | C | 0.51439在间隔 [0.5, 0.7) |
2 | [0.5,0.52] | A | 0.51439在间隔 [0.5, 0.7)的第1个1/10 |
3 | [0.514,0.52] | D | 0.51439在间隔[0.5, 0.52)的第7个1/10 |
4 | [0.514,0.5146] | A | 0.51439在间隔[0.514, 0.52]的第1个1/10 |
5 | [0.5143,0.51442] | C | 0.51439在间隔[0.514, 0.5146]的第5个1/10 |
6 | [0.514384,0.51442] | D | 0.51439在间隔[0.5143, 0.51442]的第7个1/10 |
7 | [0.51439,0.5143948] | B | 0.51439在间隔[0.51439,0.5143948]的第1个1/10 |
8 | 译码的消息:C A D A C D B |
4、游程编码
行程编码的基本原理是:用一个符号值或串长代替具有相同值的连续符号(连续符号构成了一段连续的“行程”。行程编码因此而得名),使符号长度少于原始数据的长度。只在各行或者各列数据的代码发生变化时,一次记录该代码及相同代码重复的个数,从而实现数据的压缩。
常见的游程编码格式包括TGA,Packbits,PCX以及ILBM。
例如:5555557777733322221111111
行程编码为:(5,6)(7,5)(3,3)(2,4)(1,7)。可见,行程编码的位数远远少于原始字符串的位数。
并不是所有的行程编码都远远少于原始字符串的位数,但行程编码也成为了一种压缩工具。
5、LZ77
算法核心是查找从前向缓冲存储器开始的最长匹配串
编码算法的具体执行步骤如下:
把编码位置设置到输入数据流的开始位置
查找窗口中最长匹配串
以(Pointer, Length,Characters)的格式输出,其中Pointer是指向窗口中匹配串的指针,Length表示匹配字符的长度,Characters是前向缓冲存储器中的不匹配的第1个字符,如果没有匹配,输出形式为( 0, 0, new symbol )
如果前向缓冲存储器不是空的,则把编码位置和窗口向前移(Length+1)个字符,然后返回到步骤2
6、LZ78
LZ78算法的基本思路与LZ77算法类似,也是利用已经处理过的编码信息,但它发生匹配时,不是保存一个三元组,而是一个二元组:匹配位置和不匹配的第一个字符。同时,还要将这个字符串保存到内存中,为此,它需要一个不断增长的编码字串表(字典)。
与LZ77相比,LZ78的最大优点是在每个编码步骤中减少了字符串比较的数目,而压缩率与LZ77类似。
如对符号串“ababcbabaaaaaaa”编码,需要的字典:
编码步骤
1 在开始时,词典和当前前缀P都是空的
2 当前字符C:=字符流中的下一个字符
3 判断P+C是否在词典中:
a.是:用C扩展P,P:= P+C
b.否:① 输出与P相对应的码字和当前字符C
② 把P+C 添加到词典中
③ 令P:=空值
4.判断字符流中是否还有字符需要编码
a.是:返回到步骤2
b.否:若P非空,输出相应码字,结束
LZ78编码示例
ABBCBCABABCAABCAAB
(0,A)(0,B)(2,C)(3,A)(2,A)(4,A)(6,B)
7、LZW
LZW算法-编码原理
Welch modification (Welch 1984)
为了解决在发送新字符时的低效问题
与LZ78的区别
LZW只输出代表词典中的缀-符串(String)的码字(code word),因此开始时词典被初始化为包含可能在字符流出现中的所有单字符
由于所有可能出现的单个字符都事先包含在词典中,每个编码步骤开始时都使用单字符前缀(one-character prefix),因此在词典中搜索的第1个缀-符串有两个字符
LZW算法-编码步骤
1:开始时的词典包含所有单字符,当前前缀P为空
2:当前字符C:=字符流中的下一个字符;
3:判断缀-符串P+C是否在词典中
(1) 是:P:= P+C; //用C扩展P
(2) 否:
① 把代表当前前缀P的码字输出到码字流;
② 把缀-符串P+C添加到词典;
③ 令P:= C; //现在的P仅包含一个字符C
4: 判断码字流中是否还有码字要译
(1) 是,就返回到步骤2;
(2) 否
① 把代表当前前缀P的码字输出到码字流;
② 结束。
LZW算法-编码示例
LZW算法-解码步骤
1:在开始译码时词典包含所有单字符
2:cW:=码字流中的第一个码字,输出String.cW到码字流。
3:先前码字pW:=当前码字cW,
4:当前码字cW:=码字流中的下一个码字。
5:判断String.cW是否在词典中
(1) 是,则:
① String.cW输出到字符流
② P:=String.pW,C:=Sting.cW的第一个字符。
③ 把P+C添加到词典。
(2) 否,则:
① P:=String.pW,C:=String.pW的第一个字符。
② 输出P+C到字符流,然后把它添加到词典中。
6: 判断码字流中是否还有码字要译
(1) 是,就返回到步骤3。
(2) 否, 结束。