一.LZ77原理
LZ77是基于字节的通用压缩算法,它的原理就是将源文件中的重复字节(即在前文中出现的重复字节)使用
(距离,长度)的二元组进行替换。
例:
mnoabczxyuvwabc123456abczxydefgh
mnoabczxyuvm(9,3)123456(18,6)defgh
二.压缩过程
- 1.将文件中一部分字节先传入自己创建的缓冲区,用于查找重复和压缩。
缓冲区分为查找缓冲区【已扫描过的数据,压缩完成】与先行缓冲区【未扫描过的数据】 - 2.创建一个哈希表结构,用于存储各字符串在缓冲区中的下标位置。
- 3.缓冲区每扫描到一个字符,将其与后两个字符组成三元字符串,将其首字符下标位置存入哈希表中。
- 4.当扫描到有相同三元字符串时,依次对哈希表中相同字符串进行匹配,匹配最长字符串,将<距离,长度>对压缩入文件。将已匹配的字符串三个一组存入哈希表。
- 5.当先行缓冲区小于一定值且文件未扫描结束时,对缓冲区进行重新填充。并更新哈希表。
三.常见问题
1.字符串最大最小匹配
1.字符串最大匹配为258
长度超过255之后,长度必须要用两个字节表示,会影响压缩率,而大部分情况下,能够匹配的长度都不会超过255,
如果让0表示匹配长度为3个字符,则一个字节最多可以表示258。
2.字符串匹配长度最小为3
压缩文件存储匹配串时使用长度,距离对存储,而长度最多匹配设置为258(3-255),也就是一个字节,
距离设置为两个字节,因为缓冲区大小设置为64k,所以需要两个字节来保存距离。如果字符串匹配长度小于3,则压缩反而变大。
2.哈希表解决哈希冲突方式
通过数组模拟哈希桶来解决。将哈希表分为两部分【_prev,_head】,
_head用来存储当前字符串下标,_prev存储冲突下标,存储位置为当前新存入字符串下标。
3.缓冲区大小
缓冲区设置为64k,GZIP认为64k时时间,空间成本较为适中。
缓冲区分为等大的左窗和右窗,当需要再次填充时,将右窗数据移入左窗,再进行填充。
4.哈希表大小
三个字符总共可以组成2^24种取值(即16M),表的个数需要2^24个,而索引大小占2个字节,总共表占32M
字节,是一个非常大的开销。随着窗口的移动,表中的数据会不断过时,维护这么大的表,会降低程序
运行的效率。因此本文哈希桶的个数设置为: 2^15(即32K)。当发生哈希冲突时,使用数组模拟哈希桶来解决。大小也为2^15。
所以,哈希表的总大小为64k。
5.哈希函数
哈希函数原则:简单、离散。哈希函数设计如下:
A(4,5) + A(6,7,8) ^ B(1,2,3) + B(4,5) + B(6,7,8) ^ C(1,2,3) + C(4,5,6,7,8)
说明:A 指 3 个字节中的第 1 个字节,B 指第 2 个字节,C 指第 3 个字节,
A(4,5) 指第一个字节的第 4,5 位二进制码,“^”是二进制位的异或操作,“+”是“连接”而不是“加”,“^”优先于“+”)
hashAddr = (hashAddr) & HASH_MASK;
HASH_MASK为WSIZE-1,&上掩码主要是为了防止哈希地址越界
6.如何区分压缩信息为字符串还是距离长度对
用标记bit位来标识,若为0,则表示字符,若为1,则表示为距离长度对。
7.压缩文件保存内容
1.压缩字符串
2.标记信息
3.标记字节长度
4.源文件字节长度
四.解压缩过程
- 读取标记,并对该标记进行分析
如果当前标记是0,表示原字符,从压缩信息读取一个字节,直接写到解压缩之后的文件中
如果当前标记是1,表示遇到(距离,长度对),从压缩信息中读取一个两个字节表示距离,再读取一个字节表示长度,构建(距离,长度)对,然后从解压缩过的结果中找出匹配长度。 - 获取下一个标记,直到所有的标记解析完。
五.源代码
1.common.h
#pragma once
typedef unsigned char UCH;
typedef unsigned short USH;
typedef unsigned long long ULL;
const USH MIN_MATCH = 3;
const USH MAX_MATCH = 258;
const USH MIN_LOOKHEAD = MAX_MATCH + MIN_MATCH + 1;
const USH WSIZE = 32 * 1024;//32k
const USH MAX_DIST = WSIZE - MIN_LOOKHEAD;
const USH HASH_BITS = 15;
const USH HASH_SIZE = (1 << HASH_BITS);
const USH HASH_MASK = HASH_SIZE - 1;
2.LZ77.h
#pragma once
#include"lz77hashtable.h"
#include"common.h"
#include<string>
class LZ77 {
public:
LZ77();
~LZ77();
void compressfile(const std::string & strfilepath);
void uncompressfile(const std::string & strfilepath);
private:
//是在查找缓冲区中进行,查找缓冲区中可能会找到多个匹配,取最长的匹配链
//输出最长匹配
//遇到环状链,解决----设置最大匹配次数MAX_DIST
USH longestmatch(USH matchhead, USH &curmatchdist,USH _start) {
UCH curmatchlen = 0;//一次匹配的长度
UCH maxmatch = curmatchlen;
UCH maxmatchcount = 255;
USH curmatchstart = 0;//当前匹配在查找缓冲区中起始位置
//在先行缓冲区查找匹配时不能超过max_dist
USH limit = _start > MAX_DIST ? _start - MAX_DIST : 0;
do {
//匹配范围
//先行缓冲区
UCH* pstart = _pwin + _start;
UCH* pend = pstart + MAX_MATCH;
//查找缓冲区的匹配串的起始
UCH * pmatchstart = _pwin + matchhead;
curmatchlen = 0;
//进行匹配
while (pstart < pend&&*pstart == *pmatchstart) {
curmatchlen++;
pstart++;
pmatchstart++;
}
//一次匹配结束
if (curmatchlen > maxmatch) {
maxmatch = curmatchlen;
curmatchstart = matchhead;
}
} while ((matchhead=_ht.getnext(matchhead))>limit&&maxmatchcount--);
curmatchdist = _start - curmatchstart;
return maxmatch;
}
//chflag :用来区分当前字节是原字符还是长度
// 0:原字符
//1:长度
//bitcount:该字节中有几位bit位已被设置
//islen:该字节是原字符还是长度
void writeflage(FILE* fout, UCH& chflag, UCH& bitcount, bool islen) {
chflag <<= 1;
if (islen) {
chflag |= 1;
}
bitcount++;
if (bitcount == 8) {
//将该字节压缩
fputc(chflag, fout);
chflag = 0;
bitcount = 0;
}
}
void mergefile( FILE* fout, ULL filesize);
void fillwindows(FILE* fin, size_t& lookahead);
private:
UCH* _pwin; //用来保存待压缩数据的缓冲区
LZhashtable _ht;
};
3.LZ77.cpp
#include"LZ77.h"
#include<iostream>
#include<assert.h>
#include<stdlib.h>
#pragma warning(disable:4996)
LZ77::LZ77()
:_pwin(new UCH[WSIZE*2])
,_ht(WSIZE){}
LZ77::~LZ77() {
delete[] _pwin;
_pwin = nullptr;
}
void LZ77::compressfile(const std::string & strfilepath) {
//如果源文件小于MIN_MATCH ,则不处理
//获取文件大小
FILE* fin = fopen(strfilepath.c_str(), "rb");
if (nullptr == fin) {
std::cout << "open false" << std::endl;
return;
}
fseek(fin, 0, SEEK_END);
ULL filesize = ftell(fin);//源文件的size
if (filesize <= MIN_MATCH) {
std::cout << "file small" << std::endl;
return;
}
//从压缩文件读取一个缓冲区的数据
fseek(fin, 0, SEEK_SET);
size_t lookahead = fread(_pwin, 1, 2 * WSIZE, fin);//实际读取多少
USH start = 0;
//与查找相关变量
USH hashaddr = 0;
USH matchhead = 0;
USH curmatchlength = 0;
USH curmatchdist = 0;
//与写标记相关变量
UCH chflag = 0;
UCH bitcount = 0;
bool islen = false;
FILE *fout = fopen("2.lzp", "wb");
assert(fout);
FILE* foutf = fopen("3.txt", "wb");
assert(foutf);
//一个一个字符计算的
//处理前两个字节
for (USH i = 0; i < MIN_MATCH - 1; ++i) {
_ht.hashfunc(hashaddr, _pwin[i]);
}
//
//压缩
while (lookahead) {
curmatchdist = 0;
curmatchlength = 0;
//1.将当前字符串插入哈希表,并获取匹配头
_ht.insert(matchhead, _pwin[start + 2],start,hashaddr);
//2.验证在查找缓冲区中是否找到匹配
if (matchhead != 0) {
//找最长匹配,带出长度距离对
curmatchlength = longestmatch(matchhead, curmatchdist,start);
}
if (curmatchlength < MIN_MATCH) {
//没找到
//将start位置字符写入压缩文件
fputc(_pwin[start], fout);
//写当前原字符对应的标记
writeflage(foutf,chflag,bitcount,false);
++start;
lookahead--;
}
else {
//找到了
//将长度距离对写入压缩文件
//先写长度,在写距离。为了和Huffman结合
UCH chlen = curmatchlength - 3;
fputc(chlen, fout);//最少是3个,所以用0表示3
fwrite(&curmatchdist, sizeof(curmatchdist), 1, fout);
//写标记
writeflage(foutf, chflag, bitcount, true);
lookahead -= curmatchlength;//更新先行缓冲区中剩余字节数
//将已经匹配的字符串按三个一组将其插入到哈希表中
--curmatchlength;//当前字符串已经插入
while (curmatchlength) {
start++;
_ht.insert(matchhead, _pwin[start+2], start,hashaddr);
--curmatchlength;
}
start++;
}
//检测先行缓冲区中剩余字符个数
if ((lookahead <= MIN_LOOKHEAD)&&(!feof(fin))) {
fillwindows(fin,lookahead);
}
}
//标记位数不足8位
if (bitcount > 0 && bitcount < 8) {
chflag <<= (8 - bitcount);
fputc(chflag,foutf);
}
//将数据文件+标记文件合并
fflush(foutf);
mergefile(fout, filesize);
fclose(fin);
fclose(fout);
fclose(foutf);
}
void LZ77::uncompressfile(const std::string & strfilepath) {
//打开压缩,标记文件指针
FILE* fin1 = fopen(strfilepath.c_str(), "rb");
if (nullptr == fin1) {
std::cout << "open false" << std::endl;
return;
}
FILE* fin2 = fopen(strfilepath.c_str(), "rb");
if (nullptr == fin2) {
std::cout << "open false" << std::endl;
return;
}
//获取源文件大小
ULL filesize = 0;
fseek(fin2, 0 - sizeof(filesize), SEEK_END);
fread(&filesize, sizeof(filesize), 1, fin2);
//获取标记字节数
size_t flagsize = 0;
fseek(fin2, 0 - sizeof(flagsize) - sizeof(filesize), SEEK_END);
fread(&flagsize, sizeof(flagsize), 1, fin2);
//将标记指针移动到标记数据的起始位置
fseek(fin2, 0 - sizeof(flagsize) - flagsize-sizeof(filesize), SEEK_END);
//开始解压缩
//写入解压缩数据
FILE* fout = fopen("4.txt", "wb");
assert(fout);
FILE* f = fopen("4.txt", "rb");//读取匹配内容
if (nullptr == f) {
std::cout << "open false" << std::endl;
return;
}
UCH bitcount = 0;
UCH chflag = 0;
UCH ch = 0;
UCH matchlen = 0;
USH matchdist = 0;
ULL encodecount = 0;
while (encodecount<filesize) {
if (bitcount == 0) {
chflag = fgetc(fin2);
bitcount = 8;
}
if (chflag & 0x80) {
//是len
matchlen = fgetc(fin1) + 3;
encodecount += matchlen;
fflush(fout);//所以得清空缓冲区
fread(&matchdist, sizeof(matchdist), 1, fin1);
//定位文件指针
fseek(f, 0-matchdist, SEEK_END);
while (matchlen) {
//ch为255,因为数据在缓冲区,还没写入
ch = fgetc(f);
fputc(ch, fout);
matchlen--;
fflush(fout);
}
}
else {
//原字符
ch = fgetc(fin1);
fputc(ch, fout);
encodecount++;
}
chflag <<= 1;
bitcount--;
}
fclose(fin1);
fclose(fin2);
fclose(fout);
fclose(f);
}
void LZ77::mergefile(FILE* fout,ULL filesize) {
FILE* finf = fopen("3.txt", "rb");
size_t flagsize = 0;
UCH* preadbuff = new UCH[1024];
while (true) {
size_t rdsize = fread(preadbuff, 1, 1024, finf);
if (0 == rdsize)
break;
fwrite(preadbuff, 1, rdsize, fout);
flagsize += rdsize;
}
fwrite(&flagsize, sizeof(flagsize), 1, fout);
fwrite(&filesize, sizeof(filesize), 1, fout);
delete[] preadbuff;
fclose(finf);
}
void LZ77::fillwindows(FILE* fin,size_t& lookahead){
//将右窗口数据搬移到左窗口
memcpy(_pwin, _pwin + WSIZE, WSIZE);
//更新哈希表
_ht.update();
//head prev 中保存的下标
//大于wsize -wsize
//小于wsize 置为0
//读取wsize个数据放置到右窗口
if (!feof(fin)) {
lookahead += fread(_pwin + WSIZE, 1, WSIZE, fin);
}
}
4.lz77hashtable.h
#pragma once
#include"common.h"
class LZhashtable {
public:
LZhashtable(USH size);
~LZhashtable();
void insert(USH& matchhead, UCH ch, USH pos, USH& hashaddr);
void hashfunc(USH& hashaddr, UCH ch);
USH H_SHIFT();
USH getnext(USH matchhead);
void update();
private:
USH * _prev;
USH * _head;
};
5.lzhashtable.cpp
#include"lz77hashtable.h"
#include<string.h>
LZhashtable::LZhashtable(USH size)
:_prev(new USH[size*2])
,_head(_prev+size)
{
memset(_prev, 0, size * 2 * sizeof(USH));
}
LZhashtable::~LZhashtable(){
delete _prev;
_prev = nullptr;
}
//ch :本次匹配的字符串(三个字符)的最后一个字符
//本次的哈希地址是在上一次的基础上算出来的
void LZhashtable::hashfunc(USH& hashaddr, UCH ch) {
hashaddr = (((hashaddr) << H_SHIFT()) ^ (ch))&HASH_MASK;
}
USH LZhashtable::H_SHIFT() {
return (HASH_BITS + MIN_MATCH - 1) / MIN_MATCH;
}
void LZhashtable::insert(USH& matchhead, UCH ch, USH pos, USH& hashaddr) {
hashfunc(hashaddr, ch);
//找到离当前匹配字符最近的匹配链
matchhead = _head[hashaddr];
//pos可能会超过32k,&MASK目的,不越界
_prev[pos&HASH_MASK] = _head[hashaddr];
_head[hashaddr] = pos;
}
USH LZhashtable::getnext(USH matchhead) {
return _prev[matchhead&HASH_MASK];
}
void LZhashtable::update() {
for (USH i = 0; i < WSIZE; i++) {
//先更新head
if (_head[i] >= WSIZE) {
_head[i] -= WSIZE;
}
else
_head[i] = 0;
//在更新prev
if (_prev[i] >= WSIZE) {
_prev[i] -= WSIZE;
}
else
_prev[i] = 0;
}
}