matlab 中的位操作,实现数据的压缩存放

作者在 2009-09-10 21:36:26 发布以下内容
clear all
clc
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
% 当 tempBitCount>8 时进行移位
arrayBitCount = 0;
arrayCount = 0;
tempBuff = 0;
%arrayBuf = 0;
%matlab中列优先
% 测试 :把  magic(5) 中的 25 个数据按照 6 bits 方式记录
data = magic(5);
[row,col] = size(data) ;

fid = fopen('magic5.dat','wb');

for i = 1:col
    for j = 1:row
        curData = data(j,i)
        curNum = 5;
        % 存入一个一维uint8数组arrayBuf
        tempBuff = bitshift(tempBuff,curNum)+bitand(bitshift(1,curNum)-1,curData);
        arrayBitCount = arrayBitCount + curNum;
        while(arrayBitCount>7)
            arrayBitCount = arrayBitCount - 8;  % 每次 8 bits
            tempData = bitshift(bitand(bitshift(255,arrayBitCount),tempBuff),-arrayBitCount);
            %tempData =  bitand(bitshift(255,arrayBitCount),tempBuff)/bitshift(1,arrayBitCount)
            fwrite(fid,tempData,'integer*1');  % 把最高的 8 bits写入文件
            arrayCount = arrayCount+1; %记录数组中,数组下标偏移
            arrayBuf(arrayCount) = tempData;
            tempBuff = bitand(tempBuff,bitshift(1,arrayBitCount)-1); % 将高8bits清空
        end % end while
    end % j
end % i

if(arrayBitCount>0)
    tempData = bitshift(tempBuff,7-arrayBitCount);
    fwrite(fid,tempData,'integer*1');
    arrayCount = arrayCount+1; %记录数组中,数组下标偏移
    arrayBuf(arrayCount) = tempData;
end;
fclose(fid);

由于magic(5)产生的数据最大值为 25,因此使用 5bits 就能完整的存放一个数据,这样实际需要的储存空间只需要
25*5/8个字节就足够!
技术 | 阅读 4371 次
文章评论,共6条
vfdff(作者)
2009-09-10 21:48
1
&gt;&gt; load dd.txt<br />
&gt;&gt; load ddd.dat<br />
load 只能导入文本文件,而不能导入二进制文件<br />
二进制文件使用fread读取
vfdff(作者)
2009-09-10 22:29
2
integer*1 对应的是 int8 类型,也就是对于&gt;7f 的数据,matlab 会把 7f 写入文件 ,即int8 最大值
vfdff(作者)
2009-09-10 22:36
3
fread(fid,1,'uint8=&gt;uint8') 即默认的fread(fid,1,'uint8=&gt;uint8',0)表示数据之间无间隔符
vfdff(作者)
2009-09-12 11:51
4
标签: 无标签<br />
Matlab文件和数据的导入与导出 4<br />
<br />
表13-2&nbsp;&nbsp;dlmwrite函数的可用参数<br />
参&nbsp;&nbsp;数&nbsp;&nbsp;名<br />
功&nbsp;&nbsp;&nbsp; 能<br />
delimiter<br />
用于指定分隔符<br />
newline<br />
用于指定换行符,可以选择“pc”或者“unix”<br />
roffset<br />
行偏差,指定文件第一行的位置,roffset 的基数为 0<br />
coffset<br />
列偏差,指定文件第一列的位置,coffset 的基数为 0<br />
precision<br />
指定精确度,可以指定精确维数,或者采用 c 语言的格式,如“%10.5f”<br />
  <br />
  3. textread,textscan<br />
  当文件的格式已知时,可以利用textread函数和textscan函数读入。这里只介绍这两个函数应用的实例。<br />
  例13-5&nbsp;&nbsp;通过%读入文件,按照原有格式读取。<br />
文件的内容为:<br />
  Sally&nbsp;&nbsp;&nbsp; Level1 12.34 45 Yes<br />
在命令窗口中输入:<br />
  &gt;&gt; [names, types, x, y, answer] = textread('D:\mat.txt','%s %s %f %d %s', 1)<br />
得到结果为:<br />
  names =<br />
  &nbsp;&nbsp;&nbsp; 'Sally'<br />
  types =<br />
  &nbsp;&nbsp;&nbsp; 'Level1'<br />
  x =<br />
  &nbsp;&nbsp;&nbsp;12.3400<br />
  y =<br />
  &nbsp;&nbsp;&nbsp; 45<br />
  answer =<br />
  &nbsp;&nbsp;&nbsp; 'Yes'<br />
  <br />
  <br />
  例13-6&nbsp;&nbsp;函数csvread和csvwrite的应用<br />
  Sally Level1 12.34 45 1.23e10 inf NaN Yes<br />
  Joe&nbsp;&nbsp;Level2 23.54 60 9e19 -inf 0.001 No<br />
  Bill&nbsp;&nbsp;Level3 34.90 12 2e5 10 100 No<br />
  &gt;&gt; fid = fopen('D:\mat.txt');<br />
  &gt;&gt; C = textscan(fid, '%s %s %f32 %d8 %u %f %f %s');<br />
  &gt;&gt; fclose(fid);<br />
13.3&nbsp;&nbsp;低级文件I/O<br />
  本节介绍一些基本的文件操作,这些操作如表13-3所示。<br />
表13-3&nbsp;&nbsp;MATLAB的基本文件操作<br />
函&nbsp;&nbsp;&nbsp; 数<br />
功&nbsp;&nbsp;&nbsp; 能<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;fclose<br />
关闭打开的文件<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;feof<br />
判断是否为文件结尾<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;ferror<br />
文件输入输出中的错误查找<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;fgetl<br />
读入一行,忽略换行符<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;fgets<br />
读入一行,直到换行符<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;fopen<br />
打开文件,或者获取打开文件的信息<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;fprintf<br />
格式化输入数据到文件<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;fread<br />
从文件中读取二进制数据<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;frewind<br />
将文件的位置指针移至文件开头位置<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;fscanf<br />
格式化读入<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;fseek<br />
设置文件位置指针<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;ftell<br />
文件位置指针<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;fwrite<br />
向文件中写入数据<br />
  <br />
  下面重点介绍函数fprintf。该函数的调用格式如下:<br />
  count = fprintf(fid, format, A, ...),该语句将矩阵A及后面其他参数中数字的实部以 format 指定的格式写入到fid指定的文件中,返回写入数据的字节数。<br />
  上面语句中,参数format由%开头,共可由4个部分组成,分别如下:<br />
● 标记(flag),为可选部分。<br />
● 宽度和精度指示,为可选部分。<br />
● 类型标志符,为可选部分。<br />
● 转换字符,为必需部分。<br />
  1. 标记<br />
  标记用于控制输出的对齐方式,可以选择的内容如表13-4所示。<br />
表13-4&nbsp;&nbsp;标记的可选内容<br />
函&nbsp;&nbsp;&nbsp; 数<br />
功&nbsp;&nbsp;&nbsp; 能<br />
示&nbsp;&nbsp;&nbsp; 例<br />
  负号(-)<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;在参数左侧进行判别<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;%-5.2d<br />
  加号(+)<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;在数字前添加符号<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;%+5.2d<br />
  空格<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;在数字前插入空格<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;% 5.2d<br />
  0<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;在数字前插入0<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;%05.2d<br />
  2. 宽度和精度指示<br />
  用户可以通过数字指定输出数字的宽度及精度,格式如下:<br />
● %6f,指定数字的宽度;<br />
● %6.2f,指定数字的宽度及精度;<br />
● %.2f,指定数字的精度。<br />
例13-6&nbsp;&nbsp;fprintf函数宽度和精度指示符示例。<br />
  在命令窗口中输入如下命令:<br />
  &gt;&gt; file_type = fopen('D:\type.txt','w');<br />
  &gt;&gt; fprintf(file_h, '%6.2f %12.8f\n', 1.2, -43.3);<br />
  &gt;&gt; fprintf(file_h, '%6f %12f\n', 1.2, -43.3);<br />
  &gt;&gt; fprintf(file_h, '%.2f %.8f\n', 1.2, -43.3);<br />
  &gt;&gt; fclose(file_h)<br />
  ans =<br />
  &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;0<br />
  打开该文件,其内容为:<br />
  &nbsp;&nbsp;1.20 -43.30000000<br />
  1.200000&nbsp;&nbsp;&nbsp;-43.300000<br />
  1.20 -43.30000000<br />
  从上述结果可以看出宽度和精度控制的效果。<br />
  3. 转换字符<br />
  转换字符用于指定输出的符号,可以选择的内容如表13-5所示。<br />
表13-5&nbsp;&nbsp;格式化输出的标志符及意义<br />
标&nbsp;&nbsp;志&nbsp;&nbsp;符<br />
意&nbsp;&nbsp;&nbsp; 义<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;%c<br />
输出单个字符<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;%d<br />
输出有符号十进制数<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;%e<br />
采用指数格式输出,采用小写字母 e,如:3.1415e+00<br />
(续表)&nbsp;&nbsp;<br />
标&nbsp;&nbsp;志&nbsp;&nbsp;符<br />
意&nbsp;&nbsp;&nbsp; 义<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;%E<br />
采用指数格式输出,采用大写字母 E,如:3.1415E+00<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;%f<br />
以定点数的格式输出<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;%g<br />
%e及%f的更紧凑的格式,不显示数字中无效的 0<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;%G<br />
与%g相同,但是使用大写字母E<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;%i<br />
有符号十进制数<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;%o<br />
无符号八进制数<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;%s<br />
输出字符串<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;%u<br />
无符号十进制数<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;%x<br />
十六进制数(使用小写字母a-f)<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;%X<br />
十六进制数(使用大写字母A-F)<br />
  <br />
  其中 %o、%u、%x、%X支持使用子类型,具体情况这里不再赘述。格式化输出标志符的效果见下面的例子。<br />
例13-7&nbsp;&nbsp;fprintf 格式化输出示例。<br />
  &gt;&gt; x = 0:.1:1;<br />
  &gt;&gt; y = [x; exp(x)];<br />
  &gt;&gt; fid = fopen('exp.txt', 'wt');<br />
  &gt;&gt; fprintf(fid, '%6.2f %12.8f\n', y);<br />
  &gt;&gt; fclose(fid)<br />
  ans =<br />
  &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;0<br />
  显示该文件:<br />
  &gt;&gt; type exp.txt<br />
  &nbsp;&nbsp;0.00&nbsp;&nbsp;&nbsp;1.00000000<br />
  &nbsp;&nbsp;0.10&nbsp;&nbsp;&nbsp;1.10517092<br />
  …<br />
  &nbsp;&nbsp;0.90&nbsp;&nbsp;&nbsp;2.45960311<br />
  &nbsp;&nbsp;1.00&nbsp;&nbsp;&nbsp;2.71828183<br />
  例13-9&nbsp;&nbsp;利用 fprintf 函数在显示器上输出字符串<br />
  &gt;&gt; fprintf(1,'It''s Friday.\n')<br />
  It's Friday.<br />
  在该例中,利用1表示显示器,并且用两个单引号显示单引号,使用\n进行换行。在格式化输出中,这类符号称为转义符。MATLAB中的常用转义符如表13-6所示。<br />
  <br />
  <br />
表13-6&nbsp;&nbsp;MATLAB中的常用转义符<br />
转 义 符<br />
功&nbsp;&nbsp;&nbsp; 能<br />
&nbsp;&nbsp;&nbsp;\b<br />
              退格<br />
&nbsp;&nbsp;&nbsp;\f<br />
              表格填充<br />
&nbsp;&nbsp;&nbsp;\n<br />
              换行符<br />
&nbsp;&nbsp;&nbsp;\r<br />
              回车<br />
&nbsp;&nbsp;&nbsp;\t<br />
              tab<br />
&nbsp;&nbsp;&nbsp;\\<br />
              \,反斜线<br />
&nbsp;&nbsp;&nbsp;\'' 或 ''<br />
              ',单引号<br />
&nbsp;&nbsp;&nbsp;%%<br />
              %,百分号<br />
13.4&nbsp;&nbsp;利用界面工具导入数据<br />
  除前面几节介绍的函数外,也可以通过界面工具将数据导入到工作区中。本节介绍利用工作区浏览器中的工具导入数据。<br />
  选择工作区浏览器工具栏中的“Import Data”,选择待导入的文件,这里我们选择了一个文本文件,其内容为逗号分隔的数字,打开窗口如图13-3所示。
vfdff(作者)
2009-09-12 12:22
5
% for count = 1 : length(placeSeqPost )<br />
%&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Rplace(count) = placeSeqPost(count); <br />
%&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;for ii = placeSeqPost(count)-5 : placeSeqNegative(count)+5<br />
%&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;if ( ecgInitData(ii)&gt; ecgInitData(Rplace(count)) ) % R 峰为局部最大值<br />
%&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Rplace(count) = ii;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; % 记录峰值所在位置<br />
%&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;end<br />
%&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;end<br />
% end<br />
<br />
for count = 1 : length(placeSeqPost )<br />
&nbsp;&nbsp;&nbsp; temp = ecgInitData(placeSeqPost(count)-5:placeSeqNegative(count)+5);<br />
&nbsp;&nbsp;&nbsp; Rplace(count) = placeSeqPost(count)-6 + find(temp == max(temp));&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; % 记录峰值所在位置<br />
end
vfdff(作者)
2009-10-01 12:53
6
LZ77算法的基本原理<br />
<br />
让我们熟悉一下 LZ77 算法的基本流程。<br />
<br />
1、从当前压缩位置开始,考察未编码的数据,并试图在滑动窗口中找出最长的匹配字符串,如果找到,则进行步骤 2,否则进行步骤 3。<br />
<br />
2、输出三元符号组 ( off, len, c )。其中 off 为窗口中匹配字符串相对窗口边界的偏移,len 为可匹配的长度,c 为下一个字符。然后将窗口向后滑动 len + 1 个字符,继续步骤 1。<br />
<br />
3、输出三元符号组 ( 0, 0, c )。其中 c 为下一个字符。然后将窗口向后滑动 len + 1 个字符,继续步骤 1。<br />
<br />
我们结合实例来说明。假设窗口的大小为 10 个字符,我们刚编码过的 10 个字符是:abcdbbccaa,即将编码的字符为:abaeaaabaee<br />
<br />
我们首先发现,可以和要编码字符匹配的最长串为 ab ( off = 0, len = 2 ), ab 的下一个字符为 a,我们输出三元组:( 0, 2, a )<br />
<br />
现在窗口向后滑动 3 个字符,窗口中的内容为:dbbccaaaba<br />
<br />
下一个字符 e 在窗口中没有匹配,我们输出三元组:( 0, 0, e )<br />
<br />
窗口向后滑动 1 个字符,其中内容变为:bbccaaabae<br />
<br />
我们马上发现,要编码的 aaabae 在窗口中存在( off = 4, len = 6 ),其后的字符为 e,我们可以输出:( 4, 6, e )<br />
<br />
这样,我们将可以匹配的字符串都变成了指向窗口内的指针,并由此完成了对上述数据的压缩。<br />
<br />
解压缩的过程十分简单,只要我们向压缩时那样维护好滑动的窗口,随着三元组的不断输入,我们在窗口中找到相应的匹配串,缀上后继字符 c 输出(如果 off 和 len 都为 0 则只输出后继字符 c )即可还原出原始数据。<br />
<br />
当然,真正实现 LZ77 算法时还有许多复杂的问题需要解决,下面我们就来对可能碰到的问题逐一加以探讨。<br />
<br />
编码方法<br />
<br />
我们必须精心设计三元组中每个分量的表示方法,才能达到较好的压缩效果。一般来讲,编码的设计要根据待编码的数值的分布情况而定。对于三元组的第一个分量——窗口内的偏移,通常的经验是,偏移接近窗口尾部的情况要多于接近窗口头部的情况,这是因为字符串在与其接近的位置较容易找到匹配串,但对于普通的窗口大小(例如 4096 字节)来说,偏移值基本还是均匀分布的,我们完全可以用固定的位数来表示它。<br />
<br />
编码 off 需要的位数 bitnum = upper_bound( log2( MAX_WND_SIZE ))<br />
<br />
由此,如果窗口大小为 4096,用 12 位就可以对偏移编码。如果窗口大小为 2048,用 11 位就可以了。复杂一点的程序考虑到在压缩开始时,窗口大小并没有达到 MAX_WND_SIZE,而是随着压缩的进行增长,因此可以根据窗口的当前大小动态计算所需要的位数,这样可以略微节省一点空间。<br />
<br />
对于第二个分量——字符串长度,我们必须考虑到,它在大多数时候不会太大,少数情况下才会发生大字符串的匹配。显然可以使用一种变长的编码方式来表示该长度值。在前面我们已经知道,要输出变长的编码,该编码必须满足前缀编码的条件。其实 Huffman 编码也可以在此处使用,但却不是最好的选择。适用于此处的好的编码方案很多,我在这里介绍其中两种应用非常广泛的编码。<br />
<br />
第一种叫 Golomb 编码。假设对正整数 x 进行 Golomb 编码,选择参数 m,令<br />
<br />
b = 2m<br />
<br />
q = INT((x - 1)/b)<br />
<br />
r = x - qb - 1<br />
<br />
则 x 可以被编码为两部分,第一部分是由 q 个 1 加 1 个 0 组成,第二部分为 m 位二进制数,其值为 r。我们将 m = 0, 1, 2, 3 时的 Golomb 编码表列出:<br />
<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;值 x&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; m = 0&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;m = 1&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;m = 2&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;m = 3<br />
<br />
-------------------------------------------------------------<br />
<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;1&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;0&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;0 0&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 0 00&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 0 000<br />
<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;2&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;10&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;0 1&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 0 01&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 0 001<br />
<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;3&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 110&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 10 0&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 0 10&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 0 010<br />
<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;4&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;1110&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 10 1&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 0 11&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 0 011<br />
<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;5&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;11110&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;110 0&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;10 00&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 0 100<br />
<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;6&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 111110&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;110 1&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;10 01&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 0 101<br />
<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;7&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;1111110&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;1110 0&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;10 10&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 0 110<br />
<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;8&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;11111110&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;1110 1&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;10 11&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 0 111<br />
<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;9&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 111111110&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 11110 0&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;110 00&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;10 000<br />
<br />
从表中我们可以看出,Golomb 编码不但符合前缀编码的规律,而且可以用较少的位表示较小的 x 值,而用较长的位表示较大的 x 值。这样,如果 x 的取值倾向于比较小的数值时,Golomb 编码就可以有效地节省空间。当然,根据 x 的分布规律不同,我们可以选取不同的 m 值以达到最好的压缩效果。<br />
<br />
对我们上面讨论的三元组 len 值,我们可以采用 Golomb 方式编码。上面的讨论中 len 可能取 0,我们只需用 len + 1 的 Golomb 编码即可。至于参数 m 的选择,一般经验是取 3 或 4 即可。<br />
<br />
可以考虑的另一种变长前缀编码叫做 γ 编码。它也分作前后两个部分,假设对 x 编码,令 q = int( log2x ),则编码的前一部分是 q 个 1 加一个 0,后一部分是 q 位长的二进制数,其值等于 x - 2q 。γ编码表如下:<br />
<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;值 x&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;γ编码<br />
<br />
---------------------<br />
<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;1&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;0<br />
<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;2&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;10 0<br />
<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;3&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;10 1<br />
<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;4&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 110 00<br />
<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;5&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 110 01<br />
<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;6&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 110 10<br />
<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;7&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 110 11<br />
<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;8&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;1110 000<br />
<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;9&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;1110 001<br />
<br />
其实,如果对 off 值考虑其倾向于窗口后部的规律,我们也可以采用变长的编码方法。但这种方式对窗口较小的情况改善并不明显,有时压缩效果还不如固定长编码。<br />
<br />
对三元组的最后一个分量——字符 c,因为其分布并无规律可循,我们只能老老实实地用 8 个二进制位对其编码。<br />
<br />
根据上面的叙述,相信你一定也能写出高效的编码和解码程序了。<br />
<br />
另一种输出方式<br />
<br />
LZ77 的原始算法采用三元组输出每一个匹配串及其后续字符,即使没有匹配,我们仍然需要输出一个 len = 0 的三元组来表示单个字符。试验表明,这种方式对于某些特殊情况(例如同一字符不断重复的情形)有着较好的适应能力。但对于一般数据,我们还可以设计出另外一种更为有效的输出方式:将匹配串和不能匹配的单个字符分别编码、分别输出,输出匹配串时不同时输出后续字符。<br />
<br />
我们将每一个输出分成匹配串和单个字符两种类型,并首先输出一个二进制位对其加以区分。例如,输出 0 表示下面是一个匹配串,输出 1 表示下面是一个单个字符。<br />
<br />
之后,如果要输出的是单个字符,我们直接输出该字符的字节值,这要用 8 个二进制位。也就是说,我们输出一个单个的字符共需要 9 个二进制位。<br />
<br />
如果要输出的是匹配串,我们按照前面的方法依次输出 off 和 len。对 off,我们可以输出定长编码,也可以输出变长前缀码,对 len 我们输出变长前缀码。有时候我们可以对匹配长度加以限制,例如,我们可以限制最少匹配 3 个字符。因为,对于 2 个字符的匹配串,我们使用匹配串的方式输出并不一定比我们直接输出 2 个单个字符(需要 18 位)节省空间(是否节省取决于我们采用何种编码输出 off 和 len)。<br />
<br />
这种输出方式的优点是输出单个字符的时候比较节省空间。另外,因为不强求每次都外带一个后续字符,可以适应一些较长匹配的情况。<br />
<br />
1. 原理部分:<br />
<br />
  有两种形式的重复存在于计算机数据中,zip 就是对这两种重复进行了压缩。<br />
<br />
  一种是短语形式的重复,即三个字节以上的重复,对于这种重复,zip用两个数字:1.重复位置距当前压缩位置的距离;2.重复的长度,来表示这个重复,假设这两个数字各占一个字节,于是数据便得到了压缩,这很容易理解。<br />
<br />
  一个字节有 0 - 255 共 256 种可能的取值,三个字节有 256 * 256 * 256 共一千六百多万种可能的情况,更长的短语取值的可能情况以指数方式增长,出现重复的概率似乎极低,实则不然,各种类型的数据都有出现重复的倾向,一篇论文中,为数不多的术语倾向于重复出现;一篇小说,人名和地名会重复出现;一张上下渐变的背景图片,水平方向上的像素会重复出现;程序的源文件中,语法关键字会重复出现(我们写程序时,多少次前后copy、paste?),以几十 K 为单位的非压缩格式的数据中,倾向于大量出现短语式的重复。经过上面提到的方式进行压缩后,短语式重复的倾向被完全破坏,所以在压缩的结果上进行第二次短语式压缩一般是没有效果的。<br />
<br />
  第二种重复为单字节的重复,一个字节只有256种可能的取值,所以这种重复是必然的。其中,某些字节出现次数可能较多,另一些则较少,在统计上有分布不均匀的倾向,这是容易理解的,比如一个 ASCII 文本文件中,某些符号可能很少用到,而字母和数字则使用较多,各字母的使用频率也是不一样的,据说字母 e 的使用概率最高;许多图片呈现深色调或浅色调,深色(或浅色)的像素使用较多(这里顺便提一下:png 图片格式是一种无损压缩,其核心算法就是 zip 算法,它和 zip 格式的文件的主要区别在于:作为一种图片格式,它在文件头处存放了图片的大小、使用的颜色数等信息);上面提到的短语式压缩的结果也有这种倾向:重复倾向于出现在离当前压缩位置较近的地方,重复长度倾向于比较短(20字节以内)。这样,就有了压缩的可能:给 256 种字节取值重新编码,使出现较多的字节使用较短的编码,出现较少的字节使用较长的编码,这样一来,变短的字节相对于变长的字节更多,文件的总长度就会减少,并且,字节使用比例越不均匀,压缩比例就越大。<br />
<br />
  在进一步讨论编码的要求以及办法前,先提一下:编码式压缩必须在短语式压缩之后进行,因为编码式压缩后,原先八位二进制值的字节就被破坏了,这样文件中短语式重复的倾向也会被破坏(除非先进行解码)。另外,短语式压缩后的结果:那些剩下的未被匹配的单、双字节和得到匹配的距离、长度值仍然具有取值分布不均匀性,因此,两种压缩方式的顺序不能变。<br />
<br />
  在编码式压缩后,以连续的八位作为一个字节,原先未压缩文件中所具有的字节取值不均匀的倾向被彻底破坏,成为随机性取值,根据统计学知识,随机性取值具有均匀性的倾向(比如抛硬币试验,抛一千次,正反面朝上的次数都接近于 500 次)。因此,编码式压缩后的结果无法再进行编码式压缩。<br />
<br />
  短语式压缩和编码式压缩是目前计算机科学界研究出的仅有的两种无损压缩方法,它们都无法重复进行,所以,压缩文件无法再次压缩(实际上,能反复进行的压缩算法是不可想象的,因为最终会压缩到 0 字节)。<br />
<br />
  短语式重复的倾向和字节取值分布不均匀的倾向是可以压缩的基础,两种压缩的顺序不能互换的原因也说了,下面我们来看编码式压缩的要求及方法:<br />
<br />
首先,为了使用不定长的编码表示单个字符,编码必须符合“前缀编码”的要求,即较短的编码决不能是较长编码的前缀,反过来说就是,任何一个字符的编码,都不是由另一个字符的编码加上若干位 0 或 1 组成,否则解压缩程序将无法解码。<br />
<br />
看一下前缀编码的一个最简单的例子:<br />
<br />
符号 编码<br />
<br />
A 0<br />
<br />
B 10<br />
<br />
C 110<br />
<br />
D 1110<br />
<br />
E 11110<br />
<br />
有了上面的码表,你一定可以轻松地从下面这串二进制流中分辨出真正的信息内容了:<br />
<br />
1110010101110110111100010 - DABBDCEAAB<br />
<br />
要构造符合这一要求的二进制编码体系,二叉树是最理想的选择。考察下面这棵二叉树:<br />
<br />
        根(root)<br />
<br />
       0  |   1<br />
<br />
       +-------+--------+<br />
<br />
    0  | 1   0  |  1<br />
<br />
    +-----+------+ +----+----+<br />
<br />
    |     | |     |<br />
<br />
    a      | d     e<br />
<br />
     0  |  1<br />
<br />
     +-----+-----+<br />
<br />
     |     |<br />
<br />
     b     c<br />
<br />
要编码的字符总是出现在树叶上,假定从根向树叶行走的过程中,左转为0,右转为1,则一个字符的编码就是从根走到该字符所在树叶的路径。正因为字符只能出现在树叶上,任何一个字符的路径都不会是另一字符路径的前缀路径,符合要求的前缀编码也就构造成功了:<br />
<br />
a - 00 b - 010 c - 011 d - 10 e - 11<br />
<br />
接下来来看编码式压缩的过程:<br />
<br />
为了简化问题,假定一个文件中只出现了 a,b,c,d ,e四种字符,它们的出现次数分别是<br />
<br />
a : 6次<br />
<br />
b : 15次<br />
<br />
c : 2次<br />
<br />
d : 9次<br />
<br />
e : 1次<br />
<br />
如果用定长的编码方式为这四种字符编码: a : 000 b : 001 c : 010 d : 011 e : 100<br />
<br />
那么整个文件的长度是 3*6 + 3*15 + 3*2 + 3*9 + 3*1 = 99<br />
<br />
用二叉树表示这四种编码(其中叶子节点上的数字是其使用次数,非叶子节点上的数字是其左右孩子使用次数之和):<br />
<br />
          根<br />
<br />
           |<br />
<br />
      +---------33---------+<br />
<br />
      |        |<br />
<br />
   +----32---+     +----1---+<br />
<br />
   |    |     |    |<br />
<br />
+-21-+   +-11-+    +--1--+  <br />
<br />
|   |   |   |    |   |<br />
<br />
6  15  2  9    1   <br />
<br />
(如果某个节点只有一个子节点,可以去掉这个子节点。)<br />
<br />
         根<br />
<br />
         |<br />
<br />
        +------33------+<br />
<br />
       |     |<br />
<br />
    +-----32----+     1<br />
<br />
    |      |<br />
<br />
  +--21--+  +--11--+<br />
<br />
  |   |  |   |<br />
<br />
  6   15 2    9<br />
<br />
现在的编码是: a : 000 b : 001 c : 010 d : 011 e : 1 仍然符合“前缀编码”的要求。<br />
<br />
第一步:如果发现下层节点的数字大于上层节点的数字,就交换它们的位置,并重新计算非叶子节点的值。<br />
<br />
先交换11和1,由于11个字节缩短了一位,1个字节增长了一位,总文件缩短了10位。<br />
<br />
           根<br />
<br />
            |<br />
<br />
       +----------33---------+<br />
<br />
       |        |<br />
<br />
   +-----22----+     +----11----+<br />
<br />
   |      |     |     |<br />
<br />
+--21--+    1      2     9<br />
<br />
|     |<br />
<br />
6   15<br />
<br />
再交换15和1、6和2,最终得到这样的树:<br />
<br />
           根<br />
<br />
            |<br />
<br />
       +----------33---------+<br />
<br />
       |        |<br />
<br />
     +-----18----+    +----15----+<br />
<br />
    |      |    |     |<br />
<br />
  +--3--+    15   6     9<br />
<br />
  |   |<br />
<br />
  2   1<br />
<br />
这时所有上层节点的数值都大于下层节点的数值,似乎无法再进一步压缩了。但是我们把每一层的最小的两个节点结合起来,常会发现仍有压缩余地。<br />
<br />
第二步:把每一层的最小的两个节点结合起来,重新计算相关节点的值。<br />
<br />
在上面的树中,第一、二、四三层都只有一或二个节点,无法重新组合,但第三层上有四个节点,我们把最小的3和6结合起来,并重新计算相关节点的值,成为下面这棵树。<br />
<br />
           根<br />
<br />
            |<br />
<br />
       +----------33---------+<br />
<br />
       |         |<br />
<br />
    +------9-----+    +----24----+<br />
<br />
    |      |    |     |<br />
<br />
   +--3--+    6   15    9<br />
<br />
   |   |<br />
<br />
  2  1<br />
<br />
然后,再重复做第一步。<br />
<br />
这时第二层的9小于第三层的15,于是可以互换,有9个字节增长了一位,15个字节缩短了一位,文件总长度又缩短了6位。然后重新计算相关节点的值。<br />
<br />
           根<br />
<br />
            |<br />
<br />
       +----------33---------+<br />
<br />
       |        |<br />
<br />
       15     +----18----+ <br />
<br />
            |    |<br />
<br />
         +------9-----+   9<br />
<br />
         |      |<br />
<br />
         +--3--+   6<br />
<br />
         |   |<br />
<br />
         2  1<br />
<br />
这时发现所有的上层节点都大于下层节点,每一层上最小的两个节点被并在了一起,也不可能再产生比同层其他节点更小的父节点了。<br />
<br />
这时整个文件的长度是 3*6 + 1*15 + 4*2 + 2*9 + 4*1 = 63<br />
<br />
这时可以看出编码式压缩的一个基本前提:各节点之间的值要相差比较悬殊,以使某两个节点的和小于同层或下层的另一个节点,这样,交换节点才有利益。<br />
<br />
所以归根结底,原始文件中的字节使用频率必须相差较大,否则将没有两个节点的频率之和小于同层或下层其他节点的频率,也就无法压缩。反之,相差得越悬殊,两个节点的频率之和比同层或下层节点的频率小得越多,交换节点之后的利益也越大。<br />
<br />
在这个例子中,经过上面两步不断重复,得到了最优的二叉树,但不能保证在所有情况下,都能通过这两步的重复得到最优二叉树,下面来看另一个例子:<br />
<br />
                         根<br />
<br />
                         |<br />
<br />
              +---------19--------+<br />
<br />
              |                   |<br />
<br />
      +------12------+            7<br />
<br />
      |              |<br />
<br />
  +---5---+      +---7---+<br />
<br />
  |       |      |       |<br />
<br />
+-2-+   +-3-+  +-3-+   +-4-+<br />
<br />
|   |   |   |  |   |   |   |<br />
<br />
1   1   1   2  1   2   2   2<br />
<br />
这个例子中,所有上层节点都大于等于下层节点,每一层最小的两个节点结合在了一起,但仍然可以进一步优化:<br />
<br />
                         根<br />
<br />
                         |<br />
<br />
              +---------19--------+<br />
<br />
              |                   |<br />
<br />
      +------12------+            7<br />
<br />
      |              |<br />
<br />
  +---4---+      +---8---+<br />
<br />
  |       |      |       |<br />
<br />
+-2-+   +-2-+  +-4-+   +-4-+<br />
<br />
|   |   |   |  |   |   |   |<br />
<br />
1   1   1   1  2   2   2   2<br />
<br />
通过最低一层的第4第5个节点对换,第3层的8大于第2层的7。<br />
<br />
到这里,我们得出这样一个结论:一棵最优二叉编码树(所有上层节点都无法和下层节点交换),必须符合这样两个条件:<br />
<br />
1.所有上层节点都大于等于下层节点。<br />
<br />
2.某节点,设其较大的子节点为m,较小的子节点为n,m下的任一层的所有节点都应大于等于n下的该层的所有节点。<br />
<br />
当符合这两个条件时,任一层都无法产生更小的节点去和下层节点交换,也无法产生更大的节点去和上层节点交换。<br />
<br />
上面的两个例子是比较简单的,实际的文件中,一个字节有256种可能的取值,所以二叉树的叶子节点多达256个,需要不断的调整树形,最终的树形可能非常复杂,有一种非常精巧的算法可以快速地建起一棵最优二叉树,这种算法由 D.Huffman(戴·霍夫曼)提出,下面我们先来介绍霍夫曼算法的步骤,然后再来证明通过这么简单的步骤得出的树形确实是一棵最优二叉树。<br />
<br />
霍夫曼算法的步骤是这样的:<br />
<br />
·从各个节点中找出最小的两个节点,给它们建一个父节点,值为这两个节点之和。<br />
<br />
·然后从节点序列中去除这两个节点,加入它们的父节点到序列中。<br />
<br />
重复上面两个步骤,直到节点序列中只剩下唯一一个节点。这时一棵最优二叉树就已经建成了,它的根就是剩下的这个节点。<br />
<br />
仍以上面的例子来看霍夫曼树的建立过程。<br />
<br />
最初的节点序列是这样的:<br />
<br />
a(6)  b(15)  c(2)  d(9)  e(1)<br />
<br />
把最小的c和e结合起来<br />
<br />
                   | (3)<br />
<br />
a(6)   b(15)   d(9)   +------+------+<br />
<br />
              |      |<br />
<br />
              c     e<br />
<br />
不断重复,最终得到的树是这样的:<br />
<br />
       根<br />
<br />
        |<br />
<br />
   +-----33-----+<br />
<br />
   |     |<br />
<br />
   15   +----18----+   <br />
<br />
       |       |<br />
<br />
       9  +------9-----+<br />
<br />
          |       |<br />
<br />
         6     +--3--+<br />
<br />
              |   |<br />
<br />
              2  1<br />
<br />
这时各个字符的编码长度和前面我们说过的方法得到的编码长度是相同的,因而文件的总长度也是相同的: 3*6 + 1*15 + 4*2 + 2*9 + 4*1 = 63<br />
<br />
考察霍夫曼树的建立过程中的每一步的节点序列的变化:<br />
<br />
6  15 2 9 1<br />
<br />
6  15 9 3<br />
<br />
15 9  9<br />
<br />
15 18<br />
<br />
33<br />
<br />
下面我们用逆推法来证明对于各种不同的节点序列,用霍夫曼算法建立起来的树总是一棵最优二叉树:<br />
<br />
对霍夫曼树的建立过程运用逆推法:<br />
<br />
当这个过程中的节点序列只有两个节点时(比如前例中的15和18),肯定是一棵最优二叉树,一个编码为0,另一个编码为1,无法再进一步优化。<br />
<br />
然后往前步进,节点序列中不断地减少一个节点,增加两个节点,在步进过程中将始终保持是一棵最优二叉树,这是因为:<br />
<br />
1.按照霍夫曼树的建立过程,新增的两个节点是当前节点序列中最小的两个,其他的任何两个节点的父节点都大于(或等于)这两个节点的父节点,只要前一步是最优二叉树,其他的任何两个节点的父节点就一定都处在它们的父节点的上层或同层,所以这两个节点一定处在当前二叉树的最低一层。<br />
<br />
2.这两个新增的节点是最小的,所以无法和其他上层节点对换。符合我们前面说的最优二叉树的第一个条件。<br />
<br />
3.只要前一步是最优二叉树,由于这两个新增的节点是最小的,即使同层有其他节点,也无法和同层其他节点重新结合,产生比它们的父节点更小的上层节点来和同层的其他节点对换。它们的父节点小于其他节点的父节点,它们又小于其他所有节点,只要前一步符合最优二叉树的第二个条件,到这一步仍将符合。<br />
<br />
这样一步步逆推下去,在这个过程中霍夫曼树每一步都始终保持着是一棵最优二叉树。<br />
<br />
由于每一步都从节点序列中删除两个节点,新增一个节点,霍夫曼树的建立过程共需 (原始节点数 - 1) 步,所以霍夫曼算法不失为一种精巧的编码式压缩算法。<br />
<br />
附:对于 huffman 树,《计算机程序设计艺术》中有完全不同的证明,大意是这样的:<br />
<br />
1.二叉编码树的内部节点(非叶子节点)数等于外部节点(叶子节点)数减1。<br />
<br />
2.二叉编码树的外部节点的加权路径长度(值乘以路径长度)之和,等于所有内部节点值之和。(这两条都可以通过对节点数运用数学归纳法来证明,留给大家做练习。)<br />
<br />
3.对 huffman 树的建立过程运用逆推,当只有一个内部节点时,肯定是一棵最优二叉树。<br />
<br />
4.往前步进,新增两个最小的外部节点,它们结合在一起产生一个新的内部节点,当且仅当原先的内部节点集合是极小化的,加入这个新的内部节点后仍是极小化的。(因为最小的两个节点结合在一起,并处于最低层,相对于它们分别和其他同层或上层节点结合在一起,至少不会增加加权路径长度。)<br />
<br />
5.随着内部节点数逐个增加,内部节点集合总维持极小化。<br />
<br />
2.实现部分<br />
<br />
  如果世界上从没有一个压缩程序,我们看了前面的压缩原理,将有信心一定能作出一个可以压缩大多数格式、内容的数据的程序,当我们着手要做这样一个程序的时候,会发现有很多的难题需要我们去一个个解决,下面将逐个描述这些难题,并详细分析 zip 算法是如何解决这些难题的,其中很多问题带有普遍意义,比如查找匹配,比如数组排序等等,这些都是说不尽的话题,让我们深入其中,做一番思考。<br />
<br />
我们前面说过,对于短语式重复,我们用“重复距当前位置的距离”和“重复的长度”这两个数字来表示这一段重复,以实现压缩,现在问题来了,一个字节能表示的数字大小为 0 -255,然而重复出现的位置和重复的长度都可能超过 255,事实上,二进制数的位数确定下来后,所能表示的数字大小的范围是有限的,n位的二进制数能表示的最大值是2的n次方减1,如果位数取得太大,对于大量的短匹配,可能不但起不到压缩作用,反而增大了最终的结果。针对这种情况,有两种不同的算法来解决这个问题,它们是两种不同的思路。一种称为 lz77 算法,这是一种很自然的思路:限制这两个数字的大小,以取得折衷的压缩效果。例如距离取 15 位,长度取 8 位,这样,距离的最大取值为 32 k - 1,长度的最大取值为 255,这两个数字占 23 位,比三个字节少一位,是符合压缩的要求的。让我们在头脑中想象一下 lz77 算法压缩进行时的情况,会出现有意思的模型:<br />
<br />
   最远匹配位置->          当前处理位置-><br />
<br />
───┸─────────────────╂─────────────>压缩进行方向<br />
<br />
   已压缩部分             ┃    未压缩部分<br />
<br />
  在最远匹配位置和当前处理位置之间是可以用来查找匹配的“字典”区域,随着压缩的进行,“字典”区域从待压缩文件的头部不断地向后滑动,直到达到文件的尾部,短语式压缩也就结束了。<br />
<br />
  解压缩也非常简单:<br />
<br />
         ┎────────拷贝────────┒<br />
<br />
 匹配位置    ┃          当前处理位置  ┃<br />
<br />
   ┃<──匹配长度──>┃       ┠─────∨────┨<br />
<br />
───┸──────────┸───────╂──────────┸─>解压进行方向<br />
<br />
   已解压部分              ┃    未解压部分<br />
<br />
  不断地从压缩文件中读出匹配位置值和匹配长度值,把已解压部分的匹配内容拷贝到解压文件尾部,遇到压缩文件中那些压缩时未能得到匹配,而是直接保存的单、双字节,解压时只要依次直接拷贝到文件尾部即可,直到整个压缩文件处理完毕。<br />
<br />
  lz77算法模型也被称为“滑动字典”模型或“滑动窗口”模型,由于它限制匹配的最大长度,对于某些存在大量的极长匹配的文件来说,这种折衷算法显出了缺陷。另有一种lzw算法对待压缩文件中存在大量极长匹配的情况进行了完全不同的算法设计,并且只用一个数字来表示一段短语,下面来描述一下lzw的压缩解压过程,然后来综合比较两者的适用情况。<br />
<br />
  lzw的压缩过程:<br />
<br />
1) 初始化一个指定大小的字典,把 256 种字节取值加入字典。<br />
<br />
2) 在待压缩文件的当前处理位置寻找在字典中出现的最长匹配,输出该匹配在字典中的序号。<br />
<br />
3) 如果字典没有达到最大容量,把该匹配加上它在待压缩文件中的下一个字节加入字典。<br />
<br />
4) 把当前处理位置移到该匹配后。<br />
<br />
5) 重复 2、3、4 直到文件输出完毕。<br />
<br />
  lzw 的解压过程:<br />
<br />
1) 初始化一个指定大小的字典,把 256 种字节取值加入字典。<br />
<br />
2) 从压缩文件中顺序读出一个字典序号,根据该序号,把字典中相应的数据拷贝到解压文件尾部。<br />
<br />
3) 如果字典没有达到最大容量,把前一个匹配内容加上当前匹配的第一个字节加入字典。<br />
<br />
4) 重复 2、3 两步直到压缩文件处理完毕。<br />
<br />
  从 lzw 的压缩过程,我们可以归纳出它不同于 lz77 算法的一些主要特点:<br />
<br />
1) 对于一段短语,它只输出一个数字,即字典中的序号。(这个数字的位数决定了字典的最大容量,当它的位数取得太大时,比如 24 位以上,对于短匹配占多数的情况,压缩率可能很低。取得太小时,比如 8 位,字典的容量受到限制。所以同样需要取舍。)<br />
<br />
2) 对于一个短语,比如 abcd ,当它在待压缩文件中第一次出现时,ab 被加入字典,第二次出现时,abc 被加入字典,第三次出现时,abcd 才会被加入字典,对于一些长匹配,它必须高频率地出现,并且字典有较大的容量,才会被最终完整地加入字典。相应地,lz77 只要匹配在“字典区域”中存在,马上就可以直接使用。<br />
<br />
3) 一个长匹配被加入字典的过程,是从两个字节开始,逐次增长一个字节,确定了字典的最大容量,也就间接确定了匹配的可能的最大长度。相对于 lz77 用两个数字来表示一个短语,lzw 只用一个数字来表示一个短语,因此,“字典序号”的位数可以取得多一点(二进制数多一位,意味着数值大一倍),也就是说最长匹配可以比 lz77 更长,当某些超长匹配高频率地出现,直到被完整地加入字典后,lzw将开始弥补初期的低效,逐渐显出自己的优势。<br />
<br />
  可以看出,在多数情况下,lz77 拥有更高的压缩率,而在待压缩文件中占绝大多数的是些超长匹配,并且相同的超长匹配高频率地反复出现时,lzw 更具优势,GIF 就是采用了 lzw 算法来压缩背景单一、图形简单的图片。zip 是用来压缩通用文件的,这就是它采用对大多数文件有更高压缩率的 lz77 算法的原因。<br />
<br />
  接下来 zip 算法将要解决在“字典区域”中如何高速查找最长匹配的问题。
游客请输入验证码
浏览1937000次