LZW字典编码(文末附python实现代码)

字典编码


应用最广泛的一种字典编码算法,LZW算法:可以边扫描,边编码,边发送,是一个实时的 无损压缩编码技术

1. LZW编码原理和实现算法

LZW的编码思想是不断地从字符流中提取新的字符串,通俗地理解为新“词条”,然后用“代号”也就是码字表示这个“词条”。这样一来,对字符流的编码就变成了用码字去替换字符流,生成码字流,从而达到压缩数据的目的。

LZW编码是围绕称为字典的转换表来完成的。LZW编码器通过管理这个字典完成输入与输出之间的转换

一个简单的例子

LZW编码 (Encoding) 的核心思想其实比较简单,就是把出现过的字符串映射到记号上,这样就可能用较短的编码来表示长的字符串,实现压缩,例如对于字符串:

ABABAB

可以看到子串AB在后面重复出现了,这样我们可以用一个特殊记号表示AB,例如数字2,这样原来的字符串就可以表示为:

AB22

这里我们称2是字串AB的记号(Symbol)。那么A和B有没有记号来表示?当然有,例如我们规定数字0表示A,数字1表示B。实际上最后得到的压缩后的数据应该是一个记号流 (Symbol Stream) :

0122

这样我们就有一个记号和字符串的映射表,即字典 (Dictionary) :

SymbolString
0A
1B
2AB

有了压缩后的编码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->A1->B之外并没有其它映射,2->AB是在解压缩的过程中一边加入的。这就要求压缩后的数据自己能告诉解码器,完整的字典,例如2->AB是如何生成的,在解码的过程中还原出编码时用的字典。

用上面的例子来说明,我们可以想象ABABAB编码的过程:

假设初始映射字典:0--> A, 1 --> B

  1. 遇到A,用0表示,编码为0。
  2. 遇到B,用1表示,编码为1。
  3. 发现了一个子串AB,添加映射2->AB到字典里,此时字典里的映射关系有:0--> A, 1 --> B, 2 --> AB
  4. 后面又出现了AB子串,都用2来编码。

以上过程只是一个概述,并非真正LZW编码过程,只是为了表示它的思想。可以看出最前面的AB是用来生成表项2->AB的,所以它们必须被保留在压缩编码里,作为表项2->AB生成的“第一现场”。这样在解码0122的时候,解码器首先通过01直接解析出最前面A和B,并且生成表项2->AB,这样才能将后面出现的2都解析为AB。实际上解码器是自己还原出了编码时2->AB生成的过程。

编码和解码都是从前往后步步推进的,同时生成字典,所以解码的过程也是一个不断还原编码字典的过程。解码器一边解码,向后推进,一边在之前已经解出的原始数据上重现编码的过程,构建出编码时用的字典。

2. 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开始,重新增长,重复上述过程。

这里用一个例子来说明编码的过程,对下面字符进行字典编码:

itty bitty bit bin

初始默认字典为ASCII码表,完整ASCII 对照表可查看下面链接:
ASCII对照表

这里给出上述字符串对应的ASCII:

符号space(空格)binty
ASCII值3298105110116121

ASCII编码占据了0~255的编码,所以后面往字典里添加新词条的值,是从256开始赋对映关系。

StepPCP+CP+C in dict?ActionOutput
1-iiYes更新P=i-
2ititNo添加256–>it,更新P=t105
3ttttNo添加257–>tt,更新P=t116
4tytyNo添加258–>ty,更新P=y116
5y空格y+空格No添加259–>y+空格,更新P=空格121
6空格b空格+bNo添加260–>空格+b,更新P=b32
7bibiNo添加261–>bi,更新P=i98
8ititYes更新P=it-
9ittittNo添加262–>itt,更新P=t256
10tytyYes更新P=ty-
11ty空格ty+空格No添加263–>ty+空格,更新P=空格258
12空格b空格+bYes更新P=空格+b-
13空格+bi空格+biNo添加264–>空格+bi,更新P=i260
14ititYes更新P=it-
15it空格it+空格No添加265–>it+空格,更新P=空格256
16空格b空格+bYes更新P=空格+b-
17空格+bi空格+biYes更新P=空格+bi-
18空格+bin空格+binNo添加266–>空格+bin,更新P=n264
19n----110

输出结果为:

105 116 116 121 32 98 256 258 260 256 264 110

除ASCII码表外,新添加的值有:

StringSymbol
it256
tt257
ty258
y+空格259
空格+b260
bi261
itt262
ty+空格263
空格+bi264
it+空格265
空格+bin266

这里用一个表来展示原字符串是如何对应到压缩后的编码的:

ittyspacebi t
1051161161213298256
t ysapce+bi tspace+bin
258260256264110

解码算法

解码的过程比编码复杂,其核心思想在于解码需要还原出编码时的用的字典。因此要理解解码的原理,必须分析它是如何对应编码的过程的。

下面首先给出算法:

解码器的输入是压缩后的数据,即记号流 (Symbol Stream)。类似于编码,我们仍然维护两个变量pW (previous word) 和cW (current word),后缀W的含义是word,实际上就是记号 (Symbol),一个记号就代表一个word,或者说子串。

pW表示之前刚刚解码的记号;

cW表示当前新读进来的记号。

注意cW和pW都是记号,我们用**Str(cW)Str(pW)**表示它们解码出来的原字符串。

1. 初始状态,字典里只有所有的默认项,这里就是ASCII码表。此时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步,也是最难理解的。在这一步中解码器不断地在已经破译出来的数据上,模拟编码的过程,还原出字典。我们还是结合之前的例子来说明,我们需要从记号流:

105 116 116 121 32 98 256 258 260 256 264 110

解码出:

itty bitty bit bin

解码开始:

首先读取第一个记号cW=105,解码为i,输出,赋值pW=cW=105。然后开始循环,依此读取后面的记号:

注意:第一个字符是一定能够解码成功的。也就是说第一个字符一定在默认码表里面。

SteppWcWcW in Dict?ActionOuput
1-105Yes-i
2105116YesP=i,
C=t,
P+C=it
添加256–>it
t
3116116YesP=t,
C=t,
P+C=tt
添加257–>tt
t
4116121YesP=t,
C=y,
P+C=ty
添加258–>ty
y
512132YesP=y,
C=空格,
P+C=y+空格
添加259–>y+空格
空格
63298YesP=空格,
C=b,
P+C=空格+b
添加260–>空格+b
b
798256YesP=b,
C=i,
P+C=bi
添加261–>bi
it
8256258YesP=it,
C=t,
P+C=itt
添加262–>itt
ty
9258260YesP=ty,
C=空格,
P+C=ty+空格
添加263–>ty+空格
空格+b
10260256YesP=空格+b,
C=i,
P+C=空格+bi
添加264–>空格+bi
it
11256264YesP=it,
C=空格,
P+C=it+空格
添加265–>it+空格
空格+bi
12264110YesP=空格+bi,
C=n,
P+C=空格+bin
添加266–>空格+bin
n

好了,上面就是前述编码的解码过程。但是在这个过程种,读入的cW总是存在于字典种的,下面我们举另外一个例子,同时包含读入的cW在字典和不在字典:

篇幅原因,这里只给出解码过程,编码过程就不给出了。

记号流:

0 1 3 2 3 7 2

解码出:

a b ab c ab aba c

我们手里的字典只有默认项,即:

SymbolString
0a
1b
2c

解码开始:
首先读取第一个记号cW=0,解码为a,输出,赋值pW=cW=0。然后开始循环,依此读取后面的记号:

SteppWcWcW in Dict ?ActionOutput
101YesP=a,C=b,P+C=ab,添加3->abb
213YesP=b,C=a,P+C=ba,添加4->baab
332YesP=ab,C=c,P+C=abc,添加5->abcc

好,先解码到这里,我们已经解出了前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)的第一个字符,我们可以用下图来说明:
在这里插入图片描述
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lI5CSdLQ-1652799111848)(E:\大学课程资料\信息论与编码\65411199-59cf2d266209b.webp)]

注意P+C构成的方式,取前一个符号pW,加上当前最新符号cW的第一个字符。这正好对应了编码过程中遇到P+C不在字典中的情况:将P编码为pW输出,并更新P=C,P从单字符C开始重新增长。

注意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个字符,继续往下走:

SteppWcWcW in Dict ?ActionOutput
423YesP=c,C=a,P+C=ca,添加6->caab
537NoP=ab,C=a,P+C=aba,添加7->abaaba
672YesP=aba,C=c,P+C=abac,添加8->abacc

好到此为止,后面的 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 编码的过程,对比解码过程反推,理解它的原理。这也是解码算法中最难的部分。

3. 代码实现

下图为代码运行结果:
编码:
在这里插入图片描述

解码:
在这里插入图片描述

代码链接:网盘链接

总结:

好了,LZW的编码和解码过程到此就讲解完毕了。其实它的思想本身是简单的,就是将原始数据中的子串用记号表示,类似于编一部字典。编码过程中如何切割子串,建立映射的方式,其实并不是唯一的,但是LZW算法的严格之处在于,它提供了一种方式,使得压缩后的编码能够唯一地反推出编码过程中建立的字典,从而不必将字典本身写入压缩文件。试想,如果字典也需要写入压缩文件,那它占据的体积本身就会很大,可能到最后起不到压缩的效果。

转载于:原文地址

  • 7
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值