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