作者 | 谭军一
虽然过去不能改变,“未来” 可以 !
网页数据或者图像数据往往比较大,对于传输和存储都不太友好,我们在请求静态资源时能看到 Request Headers的accept-encoding 通常会包含 gzip, deflate, br 三种格式,其中 deflate 也是 PNG 图片的核心压缩算法,它主要是由 LZ77 算法与哈夫曼编码(Huffman Coding)组成的一个无损数据压缩算法,下面分别介绍它们的基础原理与 JS 部分算法实现。
1. LZ77 算法
1.1 算法简介
LZ77 算法是由 Lempel-Ziv 在 1977 发明的,其核心思想是比对相邻区域内的数据,获取数据中尽可能多的连续重复数据。然后使用坐标加长度的方式进行替换。LZ77 本身是较为简单却非常高效的压缩算法,为了获得更大的压缩比例,它与 Base64 进行配合使用。我们在学习 LZ77 时必须了解三个关键词:
滑动窗口: 指固定长度范围(窗口大小)的地址区间,窗口内存放输入流前的多个数据,每次循环窗口向右移动。
缓冲区: 预先读取一定长度的数据内容到缓冲区,用作与窗口数据进行对比。每次循环时与滑动窗口一起向右移动
字典: 滑动窗口内的字符可以组成多个字符串,这些字符串的组合称为字典。在滑动窗口跟随指针滑动过程中,字典跟随滑动窗口不断的改变。当缓冲区内的字符串能与窗口的字符串相匹配时则进行标记,被标记的字符串通过起始坐标,距离及下一个字符替换,所以通常我们将长度大于 3 个匹配的字符串才放入字典。例如 ABCD 中包含 ABC,BCD,ABCD 这三个。
1.2 LZ77 压缩
1.2.1 压缩原理
假如现在有一串字符 ABABCBABABCAD,设置缓冲区大小为 4,滑动窗口大小为 8 个字节。在初始时前四个字符 ABAB 进入缓冲区,滑动窗口为空,此时记录下一个字符 A
当窗口向右移动两个字符时,窗口内的AB和缓冲区的 AB 匹配,则进行标记替换,其中 6 表示 AB 在滑动窗口中的位置,2 表示匹配到的字符长度,C 为下一个字符。此时指针增加匹配长度 2
继续移动一个字符,发现匹配到 BAB,则继续添加匹配标识:
以此类推,直到匹配结束:
1.2.2 数据预处理
我们通常存储或者传输过程中为 16 进制数据或者字符,在编码时需要转换成对应的 ASICII码:
const str = 'ababcbababcadababcbababcadababcbababcadababcbababcadababcbababcadababcbababcadababcbababcadababcbababcadababcbababcadababcbababcadababcbababcadababcbababcad'
const arr = str.split('').map(char => char.charCodeAt())
得到结果:
[ 97, 98, 97, 98, 99, 98, 97, 98, 97, 98, 99, 97, 100, 97, 98, 97, 98, 99, 98, 97, 98, 97, 98, 99 ...]
1.2.3 滑动窗口压缩
压缩的比例,压缩的速度与滑动窗口和缓冲区的大小密切相关,通常被压缩的文件比较大,所以需要进行分片压缩,通常单片的大小为 64K,片与片之间不会进行窗口比较。扫描字符串开始编码, 移动编码原理见 1.1.2
var X = 1<<15 /* 距离用15位二进制表示*/
for(var i=0;i<str.length;i++)
{
var max_len=0, max_off=0;// 最长匹配和位置
if( i< str.length-3) {
var d1 = String.fromCharCode(str[i],str[i+1],str[i+2]); //三字节字符串加入索引
if(dic[d1]) {
dic[d1].push(i);
}else{
dic[d1] = [i];
}
for(var k=dic[d1].length-2; k>=0; k--) {
var j = dic[d1][k];
if(j <= i - X) {
break;
}
var len = strcmp(str, i, j); //在索引位置处查找最长匹配
if(len > max_len) {
max_len = len;
max_off = i - j;
}
}
}
...
}
实现 在字符串 str 中 比较 a,b 位置处的最长匹配: