lzw 压缩算法的原理与细节思考

来源:互联网 发布:debian 还是 ubuntu 编辑:程序博客网 时间:2024/06/03 17:52
lzw 是一种无损数据压缩算法。
lzw 压缩原理:
为了简化问题,下面用的是伪代码:

1.首先初始化一个“字典”,“字典”里包含了 128 个 ASC II 码。

  var dictionary = new Array;
  for(i = 0; i < 128; i++)
  {
    dictionary[i]=String.fromCharCode(i);
  }

2.不断地在输入文件中寻找在字典中出现的最长的匹配p,并输出其在字典中的位置值到目的文件。若输入文件中下一个字符为c,把pc插入字典。
 
  StringInDictionary = input_first_char();

  while( ! AtEndOfFile )
  {

    if( search_dictionary(StringInDictionary) ) != null)
    {
        CodeInDictionary = search_dictionary(StringInDictionary);

        NextChar = input_next_char();
        StringInDictionary += NextChar;
    }
    else
    {
        Output(CodeInDictionary);
        dictionary[dictionary.length] = StringInDictionary;
        StringInDictionary = NextChar;
    }
  }


  /*在字典里搜索特定字符串*/

  function search_dictionary(str)
  {
    for( i = 0; i < dictionary.length; i ++ )
    {
        if( dictionary[i] == str )
          return i;
    }

    return null;
  }

这样就得到了压缩文件。
可以看出,压缩文件里并没有包含字典,事实上,解压缩时字典是可以根据压缩文件里的内容重建的。
下面我们来看一下解压缩的代码:

  var dictionary = new Array;
  for(i = 0; i < 128; i++)
  {
    dictionary[i] = String.fromCharCode(i);
  }

  previous_code = ReadFirstCode();
  OutPutString = dictionary[previous_code];
  Output(OutPutString);

  while( ! AtEndOfFile )
  {
    current_code = ReadNextCode();
    OutPutString = dictionary[current_code];
    Output(OutPutString);
    dictionary[dictionary.length] = dictionary[previous_code] + OutPutString.substr(0, 1);
    previous_code = current_code;
  }

可以用这个原理来压缩任何文件,gif 也是基于这样的原理,只不过字典初始化时不是存储 ASC II 码,而是 256 种(或更少)预定义的颜色值。对于通用文件压缩,字典初始化时存储一个字节的所有可能的取值(0 到 255)。

一些细节问题有
1.字典数组的长度我们限制在 4096,这样,字典的每个位置值,我们用 12 位的二进制整数就足够表示了,输出时,我们把 4 个位置值(48 位)拼凑成 3 个 unicode 字符(正好也是 48 位)输出。
2.压缩和解压缩时,有可能最后一个代码没有被输出,编程时需要注意,具体解决可以看我在前面发的完整程序。
3. 解压缩时,有可能某些代码在字典中找不到对应的解压文本,通过仔细考察压缩过程,可以知道这个代码对应的文本是dictionary [previous_code] + dictionary[previous_code].substr(0, 1)。其中 previous_code 是此代码的前一个代码。

下面我们来看一个棘手的问题:
字典应该用什么样的数据结构来组织,以提高查找匹配串的效率。
为什么用哈希表来组织字典能有效减少程序的运算量,使搜索字典的速度比遍历普通一维数组提高几千倍。

首先我们考察一下字典需要存储哪些内容:

1.前面我们知道字典需要存储 4096 个字符串(key),内容因输入文件的不同而无法预知。
2.字典还需要存储这些字符串相对应的编号(code),内容是 0 到 4095。
最直观和最容易想到的是一维数组,像这样:

dictionary[code] = key;

这样的数组只能通过遍历来搜索一个特定的 key,最坏的情况是 4096 个循环,考虑到输入文件内容的随机性,搜索一个 key 平均要循环两千多次。

那么哈希表是怎么做的呢?
哈希表首先创建一些“桶”,再把元素分散到一个个的“桶”里,根据元素的 key(而不是 code),就可以确定这个元素是在哪一个“桶”里,然后去遍历这个“桶”。
其中的关键是把元素分散到特定“桶”里的规则,其实,这个规则是由你自己定义的,一个好的规则应该是:根据 key 能够确定唯一的“桶”;分配尽可能做到均匀,能确实降低元素的密度(单个“桶”里的元素尽可能少);规则的算法尽可能简单,运算量越少越好。这个规则被称为哈希函数。

在我们的程序里,哈希表初始化时创建了 4099 个“桶”,采用的哈希函数是:keyword % 4099
其中 keyword 是由 key 所包含的单个字符的 ASC II 编码值拼接而成。
由于字典里只要存储 4096 个元素,所以“桶”里的元素的平均密度小于 1。搜索一个 key 最坏的情况仍然只是 4096 个循环(出现这种情况几乎不可能),而平均的循环次数降低到 1 次。

我们的“桶”是子数组,4099 个“桶”构成的二维数组就是我们的哈希表。

总结:hash 是分散的意思,哈希表又称为散列,它的实质是把元素按照自定的规则分散开存储,以有效降低搜索的密度。


LZW压缩算法简介
作者:宋成
描述:一篇关于LZW压缩算法简介的文章,通俗易懂,值得一看!
备注:该文章整理自软件报1998年合订本上册。


LZW 压缩算法是一种新颖的压缩方法,由Lemple-Ziv-Welch 三人共同创造,用他们的名字命名。它采用了一种先进的串表压缩不,将每个第一次出现的串放在一个串表中,用一个数字来表示串,压缩文件只存贮数字,则不存贮串,从而使图象文件的压缩效率得到较大的提高。奇妙的是,不管是在压缩还是在解压缩的过程中都能正确的建立这个串表,压缩或解压缩完成后,这个串表又被丢弃。

1.基本原理
首先建立一个字符串表,把每一个第一次出现的字符串放入串表中,并用一个数字来表示,这个数字与此字符串在串表中的位置有关,并将这个数字存入压缩文件中,如果这个字符串再次出现时,即可用表示它的数字来代替,并将这个数字存入文件中。压缩完成后将串表丢弃。如 "print" 字符串,如果在压缩时用266表示,只要再次出现,均用266表示,并将"print"字符串存入串表中,在图象解码时遇到数字266,即可从串表中查出 266所代表的字符串"print",在解压缩时,串表可以根据压缩数据重新生成。

2.实现方法
A.初始化串表
在压缩图象信息时,首先要建立一个字符串表,用以记录每个第一次出现的字符串。一个字符串表最少由两个字符数组构成,一个称为当前数组,一个称为前缀数组,因为在 GIF文件中每个基本字符串的长度通常为2(但它表示的实际字符串长度可达几百甚至上千),一个基本字符串由当前字符和它前面的字符(也称前缀)构成。前缀数组中存入字符串中的首字符,当前数组存放字符串中的尾字符,其存入位置相同,因此只要确定一个下标,就可确定它所存贮的基本字符串,所以在数据压缩时,用下标代替基本字符串。一般串表大小为4096个字节(即2 的12次方),这意味着一个串表中最多能存贮4096个基本字符串,在初始化时根据图象中色彩数目多少,将串表中起始位置的字节均赋以数字,通常当前数组中的内容为该元素的序号(即下标),如第一个元素为0,第二个元素为1,第15个元素为14 ,直到下标为色彩数目加2的元素为止。如果色彩数为256,则要初始化到第258个字节,该字节中的数值为257。其中数字256表示清除码,数字257 为图象结束码。后面的字节存放文件中每一个第一次出现的串。同样也要音乐会前缀数组初始化,其中各元素的值为任意数,但一般均将其各位置1,即将开始位置的各元素初始化为0XFF,初始化的元素数目与当前数组相同,其后的元素则要存入每一个第一次出现的字符串了。如果加大串表的长度可进一步提高压缩效率,但会降低解码速度。

B.压缩方法
了解压缩方法时,先要了解几个名词,一是字符流,二是代码流,三是当前码,四是当前前缀。字符流是源图象文件中未经压缩的图象数据;代码流是压缩后写入GIF 文件的压缩图象数据;当前码是从字符流中刚刚读入的字符;当前前缀是刚读入字符前面的字符。
GIF 文件在压缩时,不论图象色彩位数是多少,均要将颜色值按字节的单位放入代码流中,每个字节均表示一种颜色。虽然在源图象文件中用一个字节表示16色、4 色、2色时会出现4位或更多位的浪费(因为用一个字节中的4位就可以表示16色),但用LZW 压缩法时可回收字节中的空闲位。在压缩时,先从字符流中读取第一个字符作为当前前缀,再取第二个字符作为当前码,当前前缀与当前码构成第一个基本字符串(如当前前缀为A,当前码为B则此字符串即为AB),查串表,此时肯定不会找到同样字符串,则将此字符串写入串表,当前前缀写入前缀数组,当前码写入当前数组,并将当前前缀送入代码流,当前码放入当前前缀,接着读取下一个字符,该字符即为当前码了,此时又形成了一个新的基本字符串(若当前码为C,则此基本字符串为BC),查串表,若有此串,则丢弃当前前缀中的值,用该串在串表中的位置代码(即下标)作为当前前缀,再读取下一个字符作为当前码,形成新的基本字符串,直到整幅图象压缩完成。由此可看出,在压缩时,前缀数组中的值就是代码流中的字符,大于色彩数目的代码肯定表示一个字符串,而小于或等于色彩数目的代码即为色彩本身。

C.清除码
事实上压缩一幅图象时,常常要对串表进行多次初始化,往往一幅图象中出现的第一次出现的基本字符串个数会超过4096个,在压缩过程中只要字符串的长度超过了4096,就要将当前前缀和当前码输入代码流,并向代码流中加入一个清除码,初始化串表,继续按上述方法进行压缩。

D.结束码
当所有压缩完成后,就向代码流中输出一个图象结束码,其值为色彩数加1,在256色文件中,结束码为257。

E.字节空间回收
在GIF 文件输出的代码流中的数据,除了以数据包的形式存放之外,所有的代码均按单位存贮,样就有效的节省了存贮空间。这如同4位彩色(16色)的图象,按字节存放时,只能利用其中的4位,另外的4位就浪费了,可按位存贮时,每个字节就可以存放两个颜色代码了。事实上在GIF 文件中,使用了一种可变数的存贮方法,由压缩过程可看出,串表前缀数组中各元素的值颁是有规律的,以256色的GIF文件中,第258-511元素中值的范围是0-510 ,正好可用9位的二进制数表示,第512-1023元素中值的范围是0-1022,正好可用10位的二进制数表示,第1024-2047 元素中值的范围是0-2046,正好用11位的二进制数表示,第2048-4095元素中值的范围是0-4094,正好用12位的二进制数表示。用可变位数存贮代码时,基础位数为图象色彩位数加1,随着代码数的增加,位数也在加大,直到位数超过为12(此时字符串表中的字符串个数正好为2 的12次方,即4096个)。其基本方法是:每向代码流加入一个字符,就要判别此字符所在串在串表中的位置(即下标)是否超过2的当前位数次方,一旦超过,位数加1。如在4位图象中,对于刚开始的代码按5位存贮,第一个字节的低5位放第一个代码,高三位为第二个代码的低3位,第二个字节的低2位放第二个代码的高两位,依次类推。对于8 位(256色)的图象,其基础位数就为9,一个代码最小要放在两个字节。

F.压缩范围
以下为256色GIF文件编码实例,如果留心您会发现这是一种奇妙的编码方法,同时为什么在压缩完成后不再需要串表,而且还在解码时根据代码流信息能重新创建串表。
字 符 串: 1,2,1,1,1,1,2,3,4,1,2,3,4,5,9,…
当 前 码: 2,1,1,1,1,2,3,4,1,2,3,4,5,9,…
当前前缀: 1,2,1,1,260,1,258,3,4,1,258,262,4,5,…
当前数组: 2,1,1, 1, 3,4,1, 4,5,9,…
数组下标: 258,259,260,261,262,263,264,265,266,267,…
代 码 流: 1,2,1,260,258,3,4,262,4,5,…

GIF文件作为一种重要的图形图象文件格式,尽管其编码规则极复杂,但其压缩效率是极高的,特别是对某些平滑过渡的图象的图形,压缩效果更好。同时由于其在压缩过程中的对图象信息能够完整的保存,在目前流行的电子图片及电子图书中得到了广泛的应用。