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

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 是分散的意思,哈希表又称为散列,它的实质是把元素按照自定的规则分散开存储,以有效降低搜索的密度。

posted on 2010-06-29 23:34  corecible  阅读(558)  评论(0编辑  收藏  举报

导航