基于LZ77压缩原理实现压缩程序

一.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;
	}
}
  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值