原文
压缩的核心思想是用记号来代替重复出现的字符串,举个栗子: AABAA ,这个字符串中 AA 出现了2次,那么我可以用 1 这个数字来代替 AA ,那么这个字符串压缩后就成了 1B1 ,当然,这不是无损压缩,因为 1 是什么,在解压的时候是不知道的,我们可以简单的把记号和原串的对应表附在字符串后面,这样解压的时候就可以查询解压了,比如 1B1:1,AA 懂点程序的都能明白,确实可以根据这个串来还原,但是这种附一个对应表的做法怎么看都很累赘。LZW 给出了一种方式,让我们的这种做法显得不那么累赘。让我们可以边解压,边翻译出记号对应的原始串。
那么它是怎么做到的呢?
用字符串的压缩来举例,为了方便查看,我把原始串和压缩之后的结果串放在一起,这样可以让读者自己检测:
原始串: ABABABABBBABABAA
压缩串: AB02B43AA
这样一个字符串,我们来用 LZW 压缩看看。
首先我们要明白,我们的目的其实是为了找出重复的串,然后用我们自己定义的记号替换掉。
好了,让我们模拟程序的过程,来一步一步走。
先看压缩过程。
1、先读入一个字符, A 。
2、再读入一个字符, B 。
3、LZW算法以两个字符为检查单元(为什么不能更多?)这个时候判断 AB 有没有出现过,我们发现没有出现过,那么意味着我们可以用一个记号来代替 AB ,比如用 0 代替 AB 。然后我们记录下这个记号和它对应的字符串,当然,这个记录是个临时的,最后不需要我们整合进压缩的字符串中。
4、我们把这两个字母中的前一个放入压缩串中,后一个握在手上,等接下来读取的一个字符,拼成两个字母的检查单元。
5、读取下一个字符, A 。这个时候我们手上有 B ,和 A 拼起来成为 BA ,判断BA 没有出现过,用 1 代替 BA 。同样记录对应关系,然后把这个检查单元的前一个字符 B 放入压缩串,后一个字符 A 握在手上等下一个字符。
6、读取下一个字符, B 。拼起来是 AB , AB 是出现过的,我们还用 0 代替了这个串,那么这里我们的行为是用 0 代替手上的 AB ,然后不做其他的事情。现在我们手上握着的是 0 了。
7、读取下一个字符, A 。拼起来是 0A , 0 虽然是我们自己定义的记号,但是也可以看做字符,那么 0A 也没有出现过,我们用 2 代替 0A ,记录下这个对应关系,然后把 0 放进压缩串,手上握着 A 。
8、后面的过程就是如上反复了,最后我们就能够得到上面我给出的压缩串。
仔细分析一下压缩过程就发现,LZW算法的压缩过程,就是把所有出现的新串都用一个记号代替,因为记号代替的串中也可以包含别的记号,所以一个记号迭代翻译成原始字符就可能指代了很长的一串,由此来达到压缩效果。
然后我们看解压过程。解压正好和压缩是相反的,我们只有压缩串了。
压缩串: AB02B43AA
原始串: ABABABABBBABABAA
因为我们没有附加对应表的信息,所以聪明人已经想到了,想要恢复原始串,唯一可行的方式就是跟压缩时一样,边读边生成对应表,才可能不需要附加对应表。
我们来看解压过程。
1、读取一个字符, A 。
2、读取一个字符, B 。
3、为了自己解释自己,那么必须重复压缩的过程,我们也需要生成对应表,同样使用压缩的时候的方式,用两个字符作为检查单元。这里我们检查 AB 没有出现过,那么用 0 代替 AB ,这就是我们生成的第一个对应关系,跟压缩一样,记录下来,然后把 A 放入要生成的原始串,手上握着 B 。
4、读取下一个字符, 0 。你突然意识到,你居然知道 0 是什么,对!但不要慌,先不要慌着替换,因为我们需要兼顾后面的对应关系,这里我们来看 B0 ,这个串没有出现,所以这里还有一个对应关系,回想压缩的过程,我们发现,我们记录的对应关系表里,记号对应的字符串都是以正常字符结尾的,也就是结尾字符不会是另一个记号,而这里 B0 中结尾的 0 明显是个记号,所以这里的新记号 1 肯定不是 B0。那么 1 是什么呢?再看看压缩过程,我们都是用两个字符的检查单元的前一个加后一个作为新记号对应的字符串的,而后一个字符肯定是正常字符,那么这里 1 肯定是以 B 开头,后一个字符是 0 的第一个字符。我们查询已经生成了一个对应关系的表可以知道 0 是 AB ,那么 1 就是 BA 。
5、记录下 1 是 BA ,依然把 B 放入原始串,手上握着 0 。
6、读取下一个字符, 2 。这个时候看到 02 ,两个都是记号,不要慌,首先, 02这个检查单元没有出现过,那么这里必然有新的记号生成。这个记号显然就是 2 本身。仿造第5条的方法,我们知道 2 肯定是以 0 开头,后一个字符是 2 的第一个字符。这不成死循环了吗?我们压根不知道 2 是什么?但是,注意看,我们不需要知道 2 是什么,我们只要知道 2 的第一个字符是什么就可以了,我刚才不是说 2 肯定是以 0 开头吗?那么 2 的第一个字符明显就是 0 的第一个字符啊!而 0 我们是知道的,豁然开朗了, 0 是 AB ,那么 2 就是 0A 了。
6、记录下 2 是 0A ,把 0 放入原始串,手上握着 2 。当然,原始串中不能有记号,我们用 AB 换掉 0 放入原始串。
7、后面的过程也是如上反复了,最难理解的地方就在于第6条,出现一个我们不认识的记号的时候。你仔细品味一下就能理解了。你牢记一点就行了,但凡遇到新的检查单元,就必然存在新的记号出现。
理解了 LZW,我有一个问题,也是我在上面压缩过程第3条中标注的,为什么要以2个字符为检查单元,能不能更多呢?首先我们知道肯定是不能无限多的,比如如果字符串有10个字符,我们正好以10个字符作为检查单元,那么这10个字符压缩后肯定只有一个记号,那解压的时候手上就这一个记号,回天乏术,无法重构原始串。那么能不能多一点点呢?比如3个字符长度作为检查单元?关于这个问题,我仔细思考了一番,没有想到好的办法,虽然我无法做到,但我没有从原理上去给出解释,所以这里也留给大家自己思考吧。
我是个喜欢动手的人,既然 LZW的原理弄清楚了,必须要动手实践一番,我用 lua 写了一段 LZW 的编码和解码函数,然后对我喜欢的一篇文章进行编码和解码,大家可以从附件里看到编解码的过程, 1.txt 是文章的原文, 2.txt 是编码之后的内容,3.txt 是解码之后的内容,可以看到 1.txt 和 3.txt 内容完全相同,证明LZW确实做到了无损,那么是不是压缩呢?我打印了一下原文字符和编码之后的字符,当然,为了便于查看,这里就不计较我用数字去做记号,一个十位数应该当作2个字符来看这种问题,因为实际应用中是对二进制流进行操作,而不是数据类型。同时我发现,其实LZW压缩何尝不是一个加密的过程呢?虽然编码之后的数据比较有规律,容易被人破解,但是我们可以在做记号的时候自己定义一种记号改变方式,我这里是用数字来做记号,并且每次直接加 1,完全可以约定一种做记号的方式,这样只拥有 2.txt 而不知道记号改变方式的情况下,想要破解文件还是比较困难的。同时LZW编码的过程只需要遍历一次数据,虽然存储对应关系的表可能会非常巨大,但这并不影响最关键的编码速度,解码速度虽然明显比编码要慢一些,但仍然是一种可行的方式。哈哈,以后可以考虑用这种方式对文件进行加密。
当然我们不能一直无穷之境的增加码表的长度,再说内存也容不下这么长的码表。正如我之前所说的,我用了0~65534保存字节所对应的编码,0~255是系统字节的长度,256~65534是我自己定义的。如果超过了65534的话,我们这必须再次编码。即将原来所对应的编码输出,将256~65534这一段清空,再次编码就可以了。
为了记录我们是在哪里清空的,所有又在文件中写一个字符,表示 “清除”,我的程序中用的是65535表示。
Lzw 针对大量的子串多次重复出现的压缩
else if (temp == NEW_TABLE) //字典已满,重新初始化字典 :那么原字典表怎样处理呢??
{
COM.WriteChar(pre_code);
COM.WriteChar(NEW_TABLE);
COM.Intial();
pre_code = code;
}