最近整理Github上以前胡乱写的代码,发现自己还写过压缩算法,大概是不知道什么时候用来练手的。里面我实现了哈夫曼树,LZW字典和算数编码三种压缩算法,时隔几年几乎没什么印象了,尤其是后两种连原理都基本忘了,所以把它们拎出来整理一下,也算是逼自己做个回忆。
本篇先讲LZW算法,Wiki里给出的介绍和样例其实还不错,不过我在网上并没有找到很多其它的比较清晰的讲解,很多都是贴贴代码和流程图了事,所以这里我用我个人的理解,把LZW的原理再整理一遍。
一个简单的例子
LZW编码 (Encoding) 的核心思想其实比较简单,就是把出现过的字符串映射到记号上,这样就可能用较短的编码来表示长的字符串,实现压缩,例如对于字符串:
ABABAB
可以看到子串AB在后面重复出现了,这样我们可以用一个特殊记号表示AB,例如数字2,这样原来的字符串就可以表示为:
AB22
这里我们称2是字串AB的记号(Symbol)。那么A和B有没有记号来表示?当然有,例如我们规定数字0表示A,数字1表示B。实际上最后得到的压缩后的数据应该是一个记号流 (Symbol Stream) :
0122
这样我们就有一个记号和字符串的映射表,即字典 (Dictionary) :
Symbol | String |
---|---|
0 | A |
1 | B |
2 | AB |
有了压缩后的编码0122,结合字典,就能够很轻松地解码 (Decoding) 原字符串ABABAB。
当然在真正的LZW中A和B不会用数字0和1表示,而是它们的ASCII值。实际上LZW初始会有一个默认的字典,包含了所有256个8bit字符,单个字符的记号就是它自身,用数字表示就是ASCII值。在此基础上,编码过程中加入的新的记号的映射,从256开始,称为扩展表(Extended Dictionary)。在这个例子里是为了简单起见,只有两个基础字符,所以规定0表示A,1表示B,从记号2开始就是扩展项了。
字典的生成
这里有一个问题:为什么第一个AB不也用2表示?即表示为222,这样不又节省了一个记号?这个问题实际上引出的是LZW的一个核心思想,即压缩后的编码是自解释 (self-explaining) 的。什么意思?即字典是不会被写进压缩文件的,在解压缩的时候,一开始字典里除了默认的0->A和1->B之外并没有其它映射,2->AB是在解压缩的过程中一边加入的。这就要求压缩后的数据自己能告诉解码器,完整的字典,例如2->AB是如何生成的,在解码的过程中还原出编码时用的字典。
用上面的例子来说明,我们可以想象ABABAB编码的过程:
- 遇到A,用0表示,编码为0。
- 遇到B,用1表示,编码为1。
- 发现了一个子串AB,添加映射2->AB到字典里。
- 后面又出现了AB子串,都用2来编码。