LZ77文件压缩

LZ77文件压缩:

所有的压缩算法 核心都是减少原文件在内存中的存储大小
无论哪一种压缩算法,都是对原文件中的内容进行了修改
比如: 可以将重复的内容用更短的记录方法代替,或者将缩小原文件中字符的实际占用的比特位来减少了没用到的比特位。

LZ77采用的压缩原理:

就是将原文件中重复的内容用“长度距离对“进行替代,长度距离对是两个内容,即长度length,距离distance。距离表示,接下来 长度为length的内容和 和该字符前distance距离后的内容重复。

举例说明:在这里插入图片描述

什么情况下进行压缩?(也就是什么时候用距离长度对进行替换呢)

距离长度对 包含两个内容,每个内容占一个字节,也就是距离长度对压缩文件中占2个字节,所以只有当重复的内容大于等于3字节的时候,用长度距离对进行替换才能起到压缩的效果,相反如果遇到重复的单个字节就用长度距离对来替换,会使得文件越压缩越大。


思路:

1.压缩部分:
    1.首先明确,压缩过程就是把原文件中的重要的内容写到压缩文件中的过程,可以认为
      是去重的过程,所以可以使用哈希表记录重复内容的信息。
    2.在压缩过程中先读取原文件,将原文件内容按字节依次进行读取,如果是第一次出现
      就将源字符写入压缩文件,并且将该字符串信息保存到哈希表中,方便后面进行内容对比,对比
      是为了寻找最长的字符串长度,具体方法采用 闭散列 和 开散列 结合的方法。
      3.寻找到最长字符串后,就可以将“长度距离对”写入压缩文件。
    4.每次读一个字节,效率会比较低,所以每次读取一个缓冲区的内容,如果一次就能把所
      有内容读完,就是小文件,否则就是大文件。二者的主要区别在于,用不用对缓冲区中
      的滑动窗口中的数据进行搬移。
2.解压缩部分:
      1.明确解压缩就是根据一个压缩文件,要完整的还原出原文件。原文件有后缀,有大小。
        所以在上面的压缩文件中要把源文件的后缀和大小写进去。同时还有一个问题,写入
        的长度距离对  如果和 源文件中的内容一样怎么办? 所以要进行区分,也就是压缩文
        件的内容相当于双行道,既有源文件的实际内容(由未重复部分 和 最长重复部分组成)
        也有用来区分写入的是  源字符 还是 标记的标记内容(因为只有两种状态,可以用比
        特位进行区别,一个字节就相当于源文件中8个单位长度)。
      2.具体的解压缩就是一个反其道行之的过程。从压缩文件中先取出标记文件的大小,再
        得到源文件的大小,定位标记文件的位置,读取压缩内容的同时读取标记内容,如果
        确定是字符信息就直接写到解压缩文件中,如果确定是长度距离对,就在已解压的内容
        中查找重复的部分并进行还原。

具体的实现过程
压缩过程:

按照其文件大小 分成小文件 和 大文件
具体的区别: 小文件压缩不用进行数据的搬移,相较简单,所以先分析小文件的压缩。

小文件压缩:

  • 哈希表是一段连续的空间,大小是64k,后32k字节的空间作为head数组,用来存放哈希地址对应下的缓冲区下标,前32k字节作为prev数组,用来解决哈希冲突,并且通过伪链式结构定位最长匹配串在滑动窗口中有可能出现的位置。
  • 3个字节是进行匹配的标准,也就是说,先保底地用三个字符组成的字符串进行匹配,计算哈希地址,把其对应的在滑动窗口中的下标放到head数组中。
  • 定义一个参数用来记录head中的值,如果为0,说明字符串第一次出现,如果不为零,说明就发生了冲突。
  • 有冲突才意味着这出现了重复的内容,冲突肯定是由于字符串重复而导致的哈希地址相同,这时候在prev数组中 顺着 head数组中 的 内容(指的是发生冲突的哈希地址为下标),将这个内容作为 prev的下标,将其prev中的内容取出,在原来滑动窗口中进行数距离长度的匹配。
  • 小文件压缩图例:在这里插入图片描述在这里插入图片描述
    如何解决哈希冲突的
    在这里插入图片描述
    压缩文件中的具体内容
    在这里插入图片描述

大文件压缩:

  • 在小文件压缩基础上,增加了数据搬移,所谓的数据搬移是在保证没有查找的字符个数在没有达到最小的阈值之前,对原本的数据进行填充。具体做法是将滑动窗口右边的数据搬到左边去,也就是将左边的字符内容覆盖,再读取一部分内容到缓冲区中。由于窗口中的数据改变了,所以要进行哈希表的更新。
  • 大文件压缩的图例:在这里插入图片描述
    -在这里插入图片描述

大文件压缩中 滑动窗口是如何确定的
在这里插入图片描述
为什么要有最大匹配长度
在这里插入图片描述



LZ77文件压缩代码:

1.Common.h

#pragma once
#define _CRT_SECURE_NO_WARNINGS
typedef unsigned short USH;
typedef unsigned char UCH;
typedef unsigned long long ULL;


static size_t WSIZE = 32 * 1024;
static size_t MIN_MATCH = 3;
static size_t MAX_MATCH = 258;

2.HashTable.h

#pragma once
#include "Common.h"
#define _CRT_SECURE_NO_WARNINGS

class HashTable
{
public:
	HashTable(size_t size);  //构造函数
	~HashTable();
	//abcd   第二次给个d 就行
	void Insert(USH& hashAddr, UCH ch, USH pos, USH& matchHead);  //往哈希表中插入数据 
	void HashFunc(USH& hashAddr, UCH ch); //哈希函数,通过上一个哈希地址,再结合这一次的字符
	//获取在prev中的下一个重复字符串的位置,方便找到最长的匹配串
	USH GetNext(USH matchPos);  //获取下一个字符在_prev中的位置
	void Update();

private:

	USH  H_SHIFT();

private:
	USH* _prev;
	USH* _head;
	size_t _hashSize;
};

3.HashTable.cpp

#define _CRT_SECURE_NO_WARNINGS
#include "HashTable.h"
#include"Common.h"
#include <string>  //memset
const USH HASH_BITS = 15;
const USH HASH_SIZE = (1 << HASH_BITS);
const USH HASH_MASK = HASH_SIZE - 1;  //实现高位清零  0111 1111 1111 1111

HashTable::HashTable(size_t size)  //哈希表的构造
	:_prev(new USH[size*2])  //实际上prev是两个32k
	,_head(_prev+size)    //向后偏移size个
	,_hashSize(size*2)    
{
	memset(_head,0,sizeof(USH)*size);  //因为是先插到head中,再考虑哈希冲突,所以只用初始化head
}

void HashTable::Update()
{
	//更新_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;
	}
}

HashTable::~HashTable()
{
	if (_prev)
	{
		delete[] _prev;
		_prev = _head = nullptr;
	}
}
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;
}

void HashTable::Insert(USH& hashAddr, UCH ch, USH pos, USH& matchHead)
{
	HashFunc(hashAddr, ch);  //前一次的哈希地址 和 新的字符, 得到本次的哈希地址

	_prev[pos&HASH_MASK] = _head[hashAddr];  //当前哈希表中的内容 移到以pos为下标的prev数组中
	matchHead = _head[hashAddr]; //保存第一个匹配链的下标
	_head[hashAddr] = pos;
}

USH HashTable::GetNext(USH matchPos)
{
	return _prev[matchPos];
}

4.Lz77.h

#pragma once
#include "HashTable.h"
#include<string>
#define _CRT_SECURE_NO_WARNINGS
class Lz77
{
public:
	Lz77();
	~Lz77();

	void CompressFile(const std::string& filePath);
	void UnCompressFile(const std::string& filePath);

private:
	UCH LongestMatch(USH matchHead, USH& curMatchdist); //传入匹配链的头
	void WriteFlag(FILE* fOutF, UCH& chFlag, UCH& bitCount, bool IsChar);
	void GetLine(FILE* fIn, std::string& strContent);
	void FillWindow(FILE* fIn);
private:
	UCH* _pWin;   //滑动窗口
	USH _start;
	HashTable _ht;
	size_t _lookAhead;
};

5.Lz77.cpp

#pragma once
#define _CRT_SECURE_NO_WARNINGS
#include<assert.h>
#include "Lz77.h"
#include<iostream>
using namespace std;

const USH MIN_LOOKAHEAD = MAX_MATCH + 1;
const USH MAX_DIST = WSIZE - MIN_LOOKAHEAD;
Lz77::Lz77()
	: _pWin(new UCH[WSIZE *2])
	,_ht(WSIZE)
	,_start(0)
	,_lookAhead(0)
{}

Lz77::~Lz77()
{
	if (_pWin)
	{
		delete[] _pWin;
	}
}


void Lz77::CompressFile(const std::string& filePath)
{
 	FILE* fIn = fopen(filePath.c_str(), "rb");
	if (nullptr == fIn)
	{
		cout << "文件打开失败";
		return;
	}

	//获取文件的大小
	fseek(fIn, 0, SEEK_END);   //将文件指针 移动到文件末尾的位置, ftell 返回文件指针的位置
	ULL fileSize = ftell(fIn);//返回当前位置,也就是文件大小
	fseek(fIn, 0, SEEK_SET);

	//文件大小 小于3字节 不用压缩
	if (fileSize < 3)
	{
		fclose(fIn);
		return;
	}

	//插入期间 因为3个字符是最小的起始位置 最开始 从第三个字符开始插入 也就是从mno 的o开始插入到哈希表中,之后就是单个字符插入了(但实际上表示的还是3个字符)
	
	//先读取一个缓冲区的数据
	//_lookAhead:待压缩数据的个数
	 _lookAhead = fread(_pWin, 1, 2 * WSIZE, fIn);  //表示滑动窗口中的数据大小
	USH hashAddr = 0;
	for (size_t i = 0; i < MIN_MATCH-1; ++i)  //先用前两个字符,把第三个字符的第一个哈希地址求出来
	{
		_ht.HashFunc(hashAddr, _pWin[i]); //滑动窗口下标为0 1 2的元素被计算了,   返回的是2 被计算后的哈希地址
	}

	//打开一个用来存放压缩内容的文件
	FILE* fOutD = fopen("1.lzp", "wb");   //fOutDate  表示向文件中写入的是压缩数据,而不是标记
	assert(fOutD);

	//找后缀,并写源文件的后缀
	std::string postFix = filePath.substr(filePath.rfind('.'));
	postFix += '\n';

	fwrite(postFix.c_str(), 1, postFix.size(), fOutD);  // 往fOut 中一个一个字符写入后缀

	FILE* fOutF = fopen("2.lzp", "wb");  //fOutF 表示写入的是标记  0,1区分
	assert(fOutF);

	USH matchHead = 0;
	UCH  chFlag = 0;   //标记位,以字符为单位
	UCH  bitCount = 0;


	while (_lookAhead)  //滑动窗口没数据 就停止了
	{
		//将以start为首的三个字符插入到哈希表中,实际上插入的是下标
		_ht.Insert(hashAddr, _pWin[_start + 2], _start, matchHead); //根据上一个哈希地址 计算出下一个哈希地址
		//根据 带出来的  matchHead 来看有没有找到匹配   非零说明找到匹配
		USH curMatchDist = 0;
		UCH curMatchLen = 0;

		if (matchHead && (_lookAhead > MIN_LOOKAHEAD))  //滑动窗口中的剩余数据要满足最低标准
		{
			curMatchLen = LongestMatch(matchHead, curMatchDist);
		}

		//通过长度来判断有没有找到匹配
		if (curMatchLen < MIN_MATCH)
		{
			//没有匹配  那么长度 肯定小于3 的
			//没有找到匹配,就当成源字符写入
			fputc(_pWin[_start], fOutD);
			++_start;
			--_lookAhead;
			//写源字符的标记位,用0表示 false
			WriteFlag(fOutF, chFlag, bitCount, false);
		}
		else 
		{
			//写距离长度对
			//fputc(curMatchDist, fOutD);
			fwrite(&curMatchDist, 2, 1, fOutD); 
			fputc(curMatchLen, fOutD);

			//写标记位 长度距离对---用1表示
			WriteFlag(fOutF, chFlag, bitCount, true);  //两个字节内容
			_lookAhead -= curMatchLen;

			//更新哈希表---就是跳过重复的要压缩的内容,来到下一个要压缩的字符处
			curMatchLen -= 1;
			while (curMatchLen)
			{
				++_start;
				_ht.Insert(hashAddr, _pWin[_start + 2], _start, matchHead);
				curMatchLen--;
			}
			++_start;
		}
		/*
		if (0 == matchHead)
		{
			//在查找缓冲区中没有重复的字符串,jiu xie源字符
			fputc(_pWin[_start], fOutD);
			++_start;
			--_lookAhead;
			//写源字符的标记位,用0表示 false
			WriteFlag(fOutF, chFlag, bitCount, false);
		}
		else
		{
			//找最长匹配
			UCH curMatchDist = 0;
			UCH curMatchLen = LongestMatch(matchHead, curMatchDist);
			
			//写距离长度对
			fputc(curMatchDist, fOutD);
			fputc(curMatchLen, fOutD);
			
			//写标记位 长度距离对---用1表示
			WriteFlag(fOutF, chFlag, bitCount, true);

			//start += curMatchLen;  //偏移位置,偏移的这些内容是重复内容,也要插入哈希表格中
			_lookAhead -= curMatchLen;
	        //更新哈希表---就是跳过重复的要压缩的内容,来到下一个要压缩的字符处
			curMatchLen -= 1;
			while (curMatchLen)
			{
				++_start;
				_ht.Insert(hashAddr, _pWin[_start + 2], _start, matchHead);
				curMatchLen--;
			}
			++_start;
		}*/

		//窗口中数据如果不够,向窗口中填充数据
		if (_lookAhead <= MIN_LOOKAHEAD)
			FillWindow(fIn);
	}

	//最后一个标记不满8个比特位。需要特殊处理
	if (bitCount > 0 && bitCount < 8)
	{
		chFlag <<= (8 - bitCount);
		fputc(chFlag, fOutF);
	}

	fclose(fIn);
	fclose(fOutF);

	//将标记文件内容搬移到压缩文件中
	FILE* fInf = fopen("2.lzp", "rb");
	assert(fInf);
	UCH* pReadBuff = new UCH[1024];
	size_t flagSize = 0;//标记的大小
	while (true)
	{
		size_t rdSize = fread(pReadBuff, 1, 1024, fInf);
		if (rdSize == 0)
		{
			break;
		}
		flagSize += rdSize;
		fwrite(pReadBuff,1,rdSize,fOutD);// 从 pReadBuff 中读取rdsize个字节的内容,写到fOut 文件中
	}
	fclose(fInf);
	fwrite(&fileSize, sizeof(fileSize), 1, fOutD);   //
	fwrite(&flagSize, sizeof(flagSize), 1, fOutD);   //标记大小是

	fclose(fOutD);
	remove("2.lzp");

}



//matchHead -->哈希匹配链起始位置,链中可能有多个匹配串,所以需要找一条链中最长的匹配
UCH Lz77::LongestMatch(USH matchHead, USH& curMatchdist)   //matchHead是 在缓冲区的下标
{

	
	UCH curMatchLen = 0;
	UCH maxLen = 0;  //最大长度
	USH pos = 0;    //在prev中的位置
	UCH Matchchainlen = 255;
	// 因此只搜索_start左边MAX_DIST范围内的串
	USH limit = _start > MAX_DIST ? _start - MAX_DIST : 0;
	do
	{
		UCH* pStart = _pWin + _start;  //pStart 是在查找缓冲区中重复的字符串的起始位置
		UCH* pEnd = pStart + MAX_MATCH; //因为 pStart 一直往后走有可能越界
		//在查找缓冲区中找到匹配链的起始位置
		UCH* pCurStart = _pWin + matchHead;

		curMatchLen = 0;
		//找单条链的匹配长度 
		while ((pStart < pEnd) && (*pStart == *pCurStart))
		{
			pStart++;
			pCurStart++;
			curMatchLen++;
		}
		if (curMatchLen > maxLen)
		{
			maxLen = curMatchLen;
			pos = matchHead;
		}
		
	} while ((matchHead = _ht.GetNext(matchHead))>limit && Matchchainlen--);//获取下一次的匹配链,如果不是0,说明还没有找完
	curMatchdist = _start - pos;
	return maxLen;
}

void Lz77::WriteFlag(FILE* fOutF, UCH& chFlag, UCH& bitCount, bool IsChar)
{
	chFlag <<= 1;
	if (IsChar) //检测当前标记是不是距离对,如果是距离对,或上1
	{
		chFlag |= 1;
	}
	bitCount++;
	if (8 == bitCount)
	{
		fputc(chFlag, fOutF);  //写到标记文件中
		chFlag = 0;
		bitCount = 0;
	}
}

void Lz77::UnCompressFile(const std::string& filePath)
{
	string strPostFix = filePath.substr(filePath.rfind('.'));
	if (strPostFix != ".lzp")
	{
		cout << "压缩文件格式不支持";
		return;
	}

	//fInD 读取压缩数据
	FILE *fInD = fopen(filePath.c_str(), "rb");
	if (nullptr == fInD)
	{
		cout << "压缩文件打开失败";
		return;
	}

	//获取标记的大小
	size_t flagSize = 0;
	int offset = 0 - sizeof(flagSize);
	fseek(fInD, offset, SEEK_END);  //移动文件指针
	fread(&flagSize, sizeof(flagSize), 1, fInD);

	//获取原文件的大小
	ULL fileSize = 0;
	offset = 0 - (sizeof(flagSize) + sizeof(fileSize));
	fseek(fInD, offset, SEEK_END);
	fread(&fileSize, sizeof(fileSize), 1, fInD);

	//fInF :读取标记数据
	FILE* fInF = fopen(filePath.c_str(), "rb");
	assert(fInF);
	offset = 0 - (sizeof(flagSize) + sizeof(fileSize) + flagSize);
	fseek(fInF, offset, SEEK_END);

	//解压缩文件  读取源文件的后缀
	fseek(fInD, 0, SEEK_SET);
	string strUnComFileName("3");
	strPostFix = "";
	GetLine(fInD, strPostFix);

	strUnComFileName += strPostFix;
	
	//fOut:写压缩数据
	FILE* fOut = fopen(strUnComFileName.c_str(), "wb");
	assert(fOut);

	//fWin:处理长度距离对
	FILE* fWin = fopen(strUnComFileName.c_str(), "rb");
	assert(fWin);

	UCH charFlag = 0;
	char bitCount = -1;
	while (fileSize > 0)
	{
		//读取标记
		if (bitCount<0)
		{
			charFlag = fgetc(fInF);
			bitCount = 7;
		}
		// 0--->源数据   1--->长度距离对
		if (charFlag & (1 << bitCount))
		{
			//长度距离对
			USH dist;
			fread(&dist, 2, 1, fInD);
			UCH length = fgetc(fInD);	
			
			//文件IO比较浪费时间,写的时候没有直接把数据写文件中,而是写到缓冲区中,缓冲区没有满,数据就不会刷新到磁盘中
			fflush(fOut);   //刷新缓冲区
			fseek(fWin, 0 - dist, SEEK_END);
			fileSize -= length;


			while (length)
			{
				UCH ch = fgetc(fWin);
				fputc(ch, fOut);
				//文件压缩时候可能有重叠
				fflush(fOut);
				length--;
			}
	//		fflush(fOut);  必须要写到缓冲区中,而且每次都要刷新,所以写入循环中,防止字符因为单调造成后面的内容解压失败
			fseek(fOut, 0, SEEK_END);
		}
		else
		{
			USH ch = fgetc(fInD);
			fputc(ch,fOut);
			fileSize -= 1;
		}
		bitCount--;
	}
	fclose(fInD);
	fclose(fInF);
	fclose(fOut);
	fclose(fWin);
}


void Lz77::GetLine(FILE* fIn, std::string& strContent)
{
	while (!feof(fIn))
	{
		char ch = fgetc(fIn);
		if ('\n' == ch)
		{
			return;
		}
		strContent += ch;
	}
}


//填充数据
void Lz77::FillWindow(FILE* fIn) 
{
	//将右窗口中的数据搬移到左边窗口
	if (_start >= WSIZE + MAX_DIST)
	{
		memcpy(_pWin, _pWin + WSIZE, WSIZE);
		memset(_pWin + WSIZE, 0, WSIZE);  //清空右窗里的数据,防止留下的几个内容和文件未载入缓冲区的内容构成重复字符串
		_start -= WSIZE;
		
		//更新哈希表
		_ht.Update();
	}

	//向右窗口中填充数据
	if (!feof(fIn))
	{
		_lookAhead += fread(_pWin + WSIZE, 1, WSIZE, fIn);
		//if (rdSize < WSIZE )
		//{
		//	//没有bugou,将后面的字节补成0
		//	memset(_pWin + WSIZE + rdSize, 0, MIN_MATCH - 1);
		//	_lookAhead += rdSize;
		//}
	}
}

6.test.c

#include"Lz77.h"
#define _CRT_SECURE_NO_WARNINGS
int main()
{
	Lz77 lz;
	lz.CompressFile("音乐序列放大.bmp");
	lz.UnCompressFile("1.lzp");
	return 0;
}

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值