LZW原理详解(转载)
最近由于一些原因需要使用LZW,但是由于距离上次接触已经有几年了,由于身旁没有书,于是在网上找资料,发现一篇很不错的文章,转过来分享一下,顺便学习,记录一下。(转载)
原文章链接: LZW压缩算法原理解析.
LZW压缩算法原理解析
最近整理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编码的过程:
1.遇到A,用0表示,编码为0。
2.遇到B,用1表示,编码为1。
3.发现了一个子串AB,添加映射2->AB到字典里。
4.后面又出现了AB子串,都用2来编码。
以上过程只是一个概述,并非真正LZW编码过程,只是为了表示它的思想。可以看出最前面的A和B是用来生成表项2->AB的,所以它们必须被保留在压缩编码里,作为表项2->AB生成的“第一现场”。这样在解码0122的时候,解码器首先通过01直接解析出最前面A和B,并且生成表项2->AB,这样才能将后面出现的2都解析为AB。实际上解码器是自己还原出了编码时2->AB生成的过程。
编码和解码都是从前往后步步推进的,同时生成字典,所以解码的过程也是一个不断还原编码字典的过程。解码器一边解码,向后推进,一边在之前已经解出的原始数据上重现编码的过程,构建出编码时用的字典。
LZW算法详解
下面给出完整的LZW编码和解码的过程,结合一个稍微复杂一点的例子,来说明LZW的原理,重点是理解解码中的每一步是如何对应和还原编码中的步骤,并恢复编码字典的。
编码算法
编码器从原字符串不断地读入新的字符,并试图将单个字符或字符串编码为记号 (Symbol)。这里我们维护两个变量,一个是P (Previous),表示手头已有的,还没有被编码的字符串,一个是C (current),表示当前新读进来的字符。
1. 初始状态,字典里只有所有的默认项,例如0->a,1->b,2->c。此时P和C都是空的。2. 读入新的字符C,与P合并形成字符串P+C。3. 在字典里查找P+C,如果:- P+C在字典里,P=P+C。- P+C不在字典里,将P的记号输出;在字典中为P+C建立一个记号映射;更新P=C。4. 返回步骤2重复,直至读完原字符串中所有字符。
以上表示的是编码中间的一般过程,在收尾的时候有一些特殊的处理,即步骤2中,如果到达字符串尾部,没有新的C读入了,则将手头的P对应的记号输出,结束。
编码过程的核心就在于第3步,我们需要理解P究竟是什么。P是当前维护的,可以被编码为记号的子串。注意P是可以被编码为记号,但还并未输出。新的字符C不断被读入并添加到P的尾部,只要P+C仍然能在字典里找到,就不断增长更新P=P+C,这样就能将一个尽可能长的字串P编码为一个记号,这就是压缩的实现。当新的P+C无法在字典里找到时,我们没有办法,输出已有的P的编码记号,并为新子串P+C建立字典表项。然后新的P从单字符C开始,重新增长,重复上述过程。
这里用一个例子来说明编码的过程,之所以用小写的字符串是为了和P,C区分。
ababcababac
初始状态字典里有三个默认的映射:
String | Symbol |
---|---|
0 | a |
1 | b |
2 | c |
开始编码:
Step | P | C | P+C | P+C in Dict ? | Action | Output |
---|---|---|---|---|---|---|
1 | a | - | a | Yes | 更新P=a | - |
2 | a | b | ab | No | 添加3->ab,更新P=b | 0 |
3 | b | a | ba | No | 添加4->ba,更新P=a | 1 |
4 | a | b | ab | Yes | 更新P=ab | - |
5 | ab | c | abc | No | 添加5->abc,更新P=c | 3 |
6 | c | a | ca | No | 添加6->ca,更新P=a | 2 |
7 | a | b | ab | Yes | 更新P=ab | - |
8 | ab | a | aba | No | 添加7->aba,更新P=a | 3 |
9 | a | b | ab | Yes | 更新P=ab | - |
10 | ab | a | aba | Yes | 更新P=aba | - |
11 | aba | c | abac | No | 添加8->abac,更新P=c | 7 |
12 | c | - | - | - | - | 2 |
注意编码过程中的第3-4步,第7-8步以及8-10步,子串P发生了增长,直到新的P+C无法在字典中找到,则将当前的P输出,P则更新为单字符C,重新开始增长。
输出的结果为0132372,完整的字典为:
Symbol | String |
---|---|
0 | a |
1 | b |
2 | c |
3 | ab |
4 | ba |
5 | abc |
6 | ca |
7 | aba |
8 | abac |
这里用一个图来展示原字符串是如何对应到压缩后的编码的:
解码算法
解码的过程比编码复杂,其核心思想在于解码需要还原出编码时的用的字典。因此要理解解码的原理,必须分析它是如何对应编码的过程的。下面首先给出算法:
解码器的输入是压缩后的数据,即记号流 (Symbol Stream)。类似于编码,我们仍然维护两个变量pW (previous word) 和cW (current word),后缀W的含义是word,实际上就是记号 (Symbol),一个记号就代表一个word,或者说子串。pW表示之前刚刚解码的记号;cW表示当前新读进来的记号。
注意cW和pW都是记号,我们用Str(cW)和Str(pW)表示它们解码出来的原字符串。
1. 初始状态,字典里只有所有的默认项,例如0->a,1->b,2->c。此时pW和cW都是空的。
2. 读入第一个的符号cW,解码输出。注意第一个cW肯定是能直接解码的,而且一定是单个字符。
3. 赋值pW=cW。
4. 读入下一个符号cW。
5. 在字典里查找cW,如果:a. cW在字典里:(1) 解码cW,即输出 Str(cW)。(2) 令P=Str(pW),C=Str(cW)的**第一个字符**。(3) 在字典中为P+C添加新的记号映射。b. cW不在字典里:(1) 令P=Str(pW),C=Str(pW)的**第一个字符**。(2) 在字典中为P+C添加新的记号映射,这个新的记号一定就是cW。(3) 输出P+C。
6. 返回步骤3重复,直至读完所有记号。
显然,最重要的是第5步,也是最难理解的。在这一步中解码器不断地在已经破译出来的数据上,模拟编码的过程,还原出字典。我们还是结合之前的例子来说明,我们需要从记号流
0 1 3 2 3 7 2
解码出:
a b ab c ab aba c
这里我用空格表示出了记号是如何依次对应解码出来的子串的,当然在解码开始时我们根本不知道这些,我们手里的字典只有默认项,即:
Symbol | String |
---|---|
0 | a |
1 | b |
2 | c |
解码开始:
首先读取第一个记号cW=0,解码为a,输出,赋值pW=cW=0。然后开始循环,依此读取后面的记号:
Step | pW | cW | cW in Dict ? | Action | Output |
---|---|---|---|---|---|
1 | 0 | 1 | Yes | P=a,C=b,P+C=ab,添加3->ab | b |
2 | 1 | 3 | Yes | P=b,C=a,P+C=ba,添加4->ba | ab |
3 | 3 | 2 | Yes | P=ab,C=c,P+C=abc,添加5->abc | c |
好,先解码到这里,我们已经解出了前5个字符 a b ab c。一步一步走下来我们可以看出解码的思想。首先直接解码最前面的a和b,然后生成了3->ab这一映射,也就是说解码器利用前面已经解出的字符,如实还原了编码过程中字典的生成。这也是为什么第一个a和b必须保留下来,而不能直接用3来编码,因为解码器一开始根本不知道3表示ab。而第二个以及以后的ab就可以用记号3破译出来,因为此时我们已经建立了3->ab的关系。
仔细观察添加新映射的过程,就可以看出它是如何还原编码过程的。解码步骤5.a中,P=Str(pW),C=Str(cW)的第一个字符,我们可以用下图来说明:
注意P+C构成的方式,取前一个符号pW,加上当前最新符号cW的第一个字符。这正好对应了编码过程中遇到P+C不在字典中的情况:将P编码为pW输出,并更新P=C,P从单字符C开始重新增长。
到目前为止,我们只用到了解码步骤5.a的情况,即每次新读入的cW都能在字典里找到,只有这样我们才能直接解码cW输出,并拿到cW的第一个字符C,与P组成P+C。但实际上还有一种可能就是5.b中的cW不在字典里。为什么cW会不在字典里?回到例子,我们此时已经解出了5个字符,继续往下走:
Step | pW | cW | cW in Dict ? | Action | Output |
---|---|---|---|---|---|
4 | 2 | 3 | Yes | P=c,C=a,P+C=ca,添加6->ca | ab |
5 | 3 | 7 | No | P=ab,C=a,P+C=aba,添加7->aba | aba |
6 | 7 | 2 | Yes | P=aba,C=c,P+C=abac,添加8->abac | c |
好到此为止,后面的 ab aba c 也解码出来了,解码过程结束。这里最重要的就是Step-5,新读入一个cW为7,可7此时并不在字典里。当然我们事实上知道7最终应该对应aba,可是解码器应该如何反推出来?
为什么解码进行到这一步7->aba还没有被编入字典?因为解码比编码有一步的延迟,实际上aba正是由当前的P=ab,和那个还未知的cw=7的第一个字符C组成的,所以cW映射的就是这个即将新加入的子串P+C,也因此cW的第一个字符就是pW的第一个字符a,cW就是aba。
我们看到解码器在这里做了一个推理,既然cW到目前为止还没有被加入字典,可解码却偏偏遇到了,说明cW的映射并不是很早之前加入的,而是就在当前这一步。对应到编码的过程,就是新的cW映射,即7->aba刚被写进字典,紧接着后面的一个字串就用到了它。读者可以对照后半部分 ab aba c 编码的过程,对比解码过程反推,理解它的原理。这也是解码算法中最难的部分。
总结
好了,LZW的编码和解码过程到此就讲解完毕了。其实它的思想本身是简单的,就是将原始数据中的子串用记号表示,类似于编一部字典。编码过程中如何切割子串,建立映射的方式,其实并不是唯一的,但是LZW算法的严格之处在于,它提供了一种方式,使得压缩后的编码能够唯一地反推出编码过程中建立的字典,从而不必将字典本身写入压缩文件。试想,如果字典也需要写入压缩文件,那它占据的体积本身就会很大,可能到最后起不到压缩的效果。
LZW原理详解(转载)相关推荐
- spring依赖注入原理详解(转载)
spring依赖注入原理详解----转载 所谓依赖注入就是指:在运行期,由外部容器动态地将依赖对象注入到组件中.当spring容器启动后,spring容器初始化,创建并管理bean对象,以及销毁它.所 ...
- VLAN原理详解[转载] 网桥--交换机---路由器
来自:http://blog.csdn.net/phunxm/article/details/9498829 一.什么是桥接 桥接工作在OSI网络参考模型的第二层数据链路层,是一种以MAC地址来作 ...
- JPEG算法解密 JPEG原理详解 (转载 by jinchao)
(转载,个人笔记)https://www.cnblogs.com/Arvin-JIN/p/9133745.html (随便夸一下CSDN的粘贴功能好强大) 图片压缩有多重要,可能很多人可能并没有一个 ...
- 双目测距数学原理详解(转载)
版权声明:本文为博主原创文章,转载请联系作者 https://blog.csdn.net/u013832707/article/details/53781810 <div class=" ...
- 【转载】word2vec 中的数学原理详解
文章目录 一.前言 二.预备知识 三.背景知识 四.基于 Hierarchical Softmax 的模型 五.基于 Negative Sampling 的模型 六.若干源码细节 原文传送门: wor ...
- CRF(条件随机场)与Viterbi(维特比)算法原理详解
摘自:https://mp.weixin.qq.com/s/GXbFxlExDtjtQe-OPwfokA https://www.cnblogs.com/zhibei/p/9391014.html C ...
- LVS原理详解(3种工作方式8种调度算法)--老男孩
一.LVS原理详解(4种工作方式8种调度算法) 集群简介 集群就是一组独立的计算机,协同工作,对外提供服务.对客户端来说像是一台服务器提供服务. LVS在企业架构中的位置: 以上的架构只是众多企业里面 ...
- jQuery中getJSON跨域原理详解
详见:http://blog.yemou.net/article/query/info/tytfjhfascvhzxcytp28 jQuery中getJSON跨域原理详解 前几天我再开发一个叫 河蟹工 ...
- EMD算法之Hilbert-Huang Transform原理详解和案例分析
目录 Hilbert-Huang Transform 希尔伯特-黄变换 Section I 人物简介 Section II Hilbert-Huang的应用领域 Section III Hilbert ...
最新文章
- 网络编程中的关键问题总结
- JVM插码之六:jacoco插码及问题“$jacocodata 属性 Method not found: is$jacocoData”
- 利用python爬虫(part3)--正则表达式
- 利用 k8s 建立软件商店_为企业建立应用商店
- 中添加复选框_Word/excel中在方框中打钩/叉的符号,简单方便快捷
- (DAG+固定终点的最长路和最短路)硬币问题
- bci测试如何整改_基于fNIRS技术的脑机接口(BCI)
- 使用jQuery实现网页技术(无数据库)
- JavaCore HeapDump
- HTTP1.0/1.1/2.0特性对比_转
- hdu 5208 Where is Bob
- Windows 改变CMD窗口颜色!
- Intel 内部指令---AVX编程基础
- 使用组策略将用户文件夹重定向到D盘
- 神州租车——为用户提供精细化租车服务
- 身份认证的动态密码器解决方案
- C语言:typedef用法简介
- 《C语言动漫教程(中级)》
- python 语音特征提取_使用python实现语音文件的特征提取方法
- 科比自述天皇巨星之道
热门文章
- 【转】解决北京户口的六条光明大道
- 2021大三机器学习:train_test_split方法详解
- Snagit文字提取捕获技巧
- java.lang.ClassCastException: java.util.LinkedHashMap cannot be cast to com.gzl.cn.bean.Employee
- C++实现迪杰斯特拉(dijkstra)算法(最小生成树)
- N1盒子恢复官改系统最简单的ddbr大法(适用小钢炮以及各种变砖)(转载)
- matlab2016有SSE3吗,matlab r2011a正确安装后闪退,出现错误文件
- 12代酷睿安装18.04ubuntu无法识别硬盘
- win7下CSP修改方法
- ASEMI代理ADXL345BCCZ-RL7原装ADI车规级ADXL345BCCZ-RL7