编程语言
首页 > 编程语言> > 基于Huffman算法和LZ77算法的文件压缩(四)

基于Huffman算法和LZ77算法的文件压缩(四)

作者:互联网

基于Huffman算法和LZ77算法的文件压缩(四)

本文开始讲解LZ77算法,会用到哈希,哈希原理详解

我们在基于Huffman算法和LZ77算法的文件压缩(一)当中总体介绍了Huffman算法和LZ77算法的原理,本文讲解基于LZ77算法的文件压缩和解压缩

一、 什么是LZ77
1977年,两位以色列人Jacob Ziv和Abraham Lempel,发表了一篇论文《A Universal Algorithm for Sequential Data Compression》,一种通用的数据压缩算法,所谓通用压缩算法指的是这种压缩算法没有对数据的类型有什么限定,该算法奠基了今天大多数无损数据压缩的核心,为了纪念两位科学家,该算法被称为LZ77,过了一年他们又提了一个类似的算法,称为LZ78。

二、 LZ77压缩原理介绍

  1. LZ77原理介绍

LZ77是基于字节的通用压缩算法,它的原理就是将源文件中的重复字节(即在前文中出现的重复字节)使用 (offset,length,nextchar)的三元组进行替换。

比如:
源文件内容:mnoabczxyuvwabc123456abczxydefgh

在这里插入图片描述

// 最小匹配长度
static const size_t MIN_MATCH = 3;

// 最大匹配长度
// GZIP认为:长度超过255之后,长度必须要用两个字节表示,会影响压缩率,而大部分情况下,
// 能够匹配的长度都不会超过255,因此长度使用一个字节表示
// 而一个字节能够表示的范围是[0, 255], 如果让0表示匹配长度为3个字符,1表示匹配长度为 
//4个字符,...,则一个字节最多可以表示的匹配长度为255+3=258,即最长的匹配长度,
// 如果某个匹配长度超过258,则拆成两个匹配来进行表示
static const size_t MAX_MATCH = 258;

文件压缩过程:

在这里插入图片描述

在这里插入图片描述
但是真正的压缩,是在一个比较大的窗口中进行的,窗口越大,找到匹配的可能性就越大但不是无限大, 因为无限大实,存在两个问题

  1. 空间成本:窗口越大,需要的内存空间就越大
  2. 时间成本:窗口越大,查找匹配串时需要耗费的时间也就越多

因此,GZIP决定,窗口的大小取为64K,分为两部分,一个WSIZE大小为32K,如下图所示:
在这里插入图片描述

  1. 如何高效找最长匹配串
  2. 找不到怎么办?
  3. 当前向缓冲区中没有字符时或者不够三个字符时如何处理?

2. 高效查找最长匹配串

2.1 暴力求解

在这里插入图片描述

该算法的性能比较差,是一个O(N2)O(N^2)O(N2)的算法,如果待压缩文件比较大,会严重影响压缩的速度

2.2 采用哈希

使用哈希表来提高查询的效率:使用哈希“桶”保存每三个相邻字符构成的字符串中首字符的窗口索引。 压缩 过程中每遇到新字符时,进行如下操作:

  1. 利用哈希函数计算该字符与紧跟其后的两个字符构成字符串的哈希地址
  2. 将该字符串中首字符在窗口中的索引插入上述计算出哈希位置的哈希桶中,返回插入之前该桶的状态
  3. 根据2返回的状态监测是否找到匹配串,如果当前桶为空,说明未找到匹配, 否则:可能找到匹配,再定位到匹配串位置详细进行匹配即可。

关于"哈希桶",引发出一堆问题:

  1. 哈希桶的大小分析
    三个字符总共可以组成2^24 种取值(即16M),桶的个数需要 2^24个,而索引大小占2个字节,总共桶占32M 字节,是一个非常大的开销。随着窗口的移动,表中的数据会不断过时,维护这么大的表,会降低程序 运行的效率。因此本文哈希桶的个数设置为:2^15 (即32K)
// 哈希桶的个数为2^15
const USH HASH_BITS = 15;

// 哈希表的大小
const USH HASH_SIZE = (1 << HASH_BITS);
// 哈希掩码:主要作用是将右窗数据往左窗搬移时,用来更新哈希表中数据,具体参见后文
const USH HASH_MASK = HASH_SIZE - 1;
  1. 哈希表的结构

在这里插入图片描述

3.哈希函数 哈希函数原则:简单、离散。因此本文哈希函数设计如下:

// hashAddr: 上一个字符串计算出的哈希地址
// ch:当前字符
// 本次的哈希地址是在前一次哈希地址基础上,再结合当前字符ch计算出来的
// HASH_MASK为WSIZE-1,&上掩码主要是为了防止哈希地址越界
void HashTable::HashFunc(USH& hashAddr, UCH ch)
{
	hashAddr = (((hashAddr) << H_SHIFT()) ^ (ch)) &  
	                        HASH_MASK;
} 
USH HashTable::H_SHIFT()
{
	return (HASH_BITS + MIN_MATCH - 1) / MIN_MATCH;
}

4.哈希表构建(插入字符串)
哈希表的构建即将字符串插入到哈希表中,该过程伴随着压缩过程一块进行:

// hashAddr:上一次哈希地址 ch:先行缓冲区第一个字符
// pos:ch在滑动窗口中的位置 matchHead:如果匹配,保存匹配串的起始位置
void HashTable::InsertString(USH& hashAddr, UCH ch, USH pos, USH& macthHead)
{
	// 计算哈希地址
	HashFunc(hashAddr, ch);
	// 随着压缩的不断进行,pos肯定会大于WSIZE,与上WMASK保证不越界
	_prev[pos & WMASK] = _head[hashAddr];
	macthHead = _head[hashAddr];
	_head[hashAddr] = pos;
}

在这里插入图片描述

5.查找最长匹配

字符串插入后,如果matchHead为空,表示为遇到匹配串,比如第一个"abc"的插入过程;否则,表示 在查找缓冲区出现过该字符串。此时,顺着匹配链查找所有的匹配串,直到找到最长匹配。

// 功能:在当前匹配链中找最长匹配
// 参数:
// hashHead: 匹配链的起始位置
// matchStart:最长匹配串在滑动窗口中的起始位置
// 返回值:最长匹配串的长度
USH BitZip::LongestMatch(USH hashHead, USH& matchStart)
{
	// 哈希链的最大遍历长度,防止造成死循环
	int chain_length = 256;
	// 始终保持滑动窗口为WSIZE,因为最小的超前查看窗口中有	MIN_LOOKAHEAD的数据
	// 因此只搜索_start左边MAX_DIST范围内的串
	USH limit = _start > MAX_DIST ? _start - MAX_DIST : 0;
	// 待匹配字符串的最大位置

	// [pScan, strend]
	UCH* pScan = _pwin + _start;
	UCH* strend = pScan + MAX_MATCH - 1;
	// 本次链中的最佳匹配
	int bestLen = 0;
	UCH* pCurMatchStart;
	USH curMatchLen = 0;
	// 开始匹配
	do
	{
		// 从搜索区hashHead的位置开始匹配
		pCurMatchStart = _pwin + hashHead;
		while (pScan < strend && *pScan == 		*pCurMatchStart)
		{
			pScan++;
			pCurMatchStart++;
		} 
		// 本次匹配的长度和匹配的起始位置
		curMatchLen = (MAX_MATCH - 1) - (int)(strend - pScan);
		pScan = strend - (MAX_MATCH - 1);
		/*更新最佳匹配的记录*/
		if (curMatchLen > bestLen)
		{
			matchStart = hashHead;
			bestLen = curMatchLen;
		}
	} while ((hashHead = _hash._prev[hashHead & WMASK]) > limit
							&& --chain_length != 0);
	return curMatchLen;
}

通过上述方式获取到的最长匹配串,一定是最长的吗?如何优化?
“1abc23bcdefghijklm456abcdefghijklmnopq”

2.3 找不到最长匹配怎么办

2.4 滑动窗口中数据不够时怎么办?

随着滑动窗口的不断移动,右侧窗口中的数据不足MIN_LOOKAHEAD时怎么办?在压缩时,如果文件没有读
到结尾,为了保证最大匹配,必须保持look_ahead中至少有MIN_LOOKAHEAD的源数据。

在这里插入图片描述

此时,需要将右窗中的数据搬移到左窗中。

在这里插入图片描述

注意:窗口中的数据移动,此时必须更新哈希表

void LZ77::FillWindow(FILE* fIn)
{ 
	// 滑动窗口中的数据不足时
	// 把右窗中数据(32K)移到左窗
	if (_start >= WSIZE + MAX_DIST)		
	{
		memcpy(_pWin, _pWin + WSIZE, WSIZE);
		_start -= WSIZE;
		//更新哈希表,若是旧左窗的字串,则删除该词条,重置为nil,
		//注意,哈希表中越靠近头部的串,在窗口位置越靠右(就是更加新鲜),
		_ht.Update();
	} 
	size_t readSize = 0;
	if (!feof(fIn))
	{
		readSize = fread(_pWin + _start + _lookAhead, 1, WSIZE, fIn);
		if (0 == readSize)
			memset(_pWin + _start + _lookAhead, 0, MIN_MATCH - 1);
		else
			_lookAhead += readSize;
	} 
}

void HashTable::UpdateDictionary()
{
	// 更新_head数组
	for (int i = 0; i < HASH_SIZE; i++)
	{ 
		if (_head[i] >= WSIZE)
			_head[i] -= WSIZE;
		else
			_head[i] = 0;
	}
	// 更新prev数组
	for (int i = 0; i < WSIZE; i++)
	{ 
		if (_prev[i] >= WSIZE)
			_prev[i] -= WSIZE;
		else
			_prev[i] = 0;
	} 
}

标签:字符,匹配,字节,WSIZE,算法,LZ77,哈希,窗口,Huffman
来源: https://blog.csdn.net/wolfGuiDao/article/details/104771769