所有的压缩算法 核心都是减少原文件在内存中的存储大小。
无论哪一种压缩算法,都是对原文件中的内容进行了修改
比如: 可以将重复的内容用更短的记录方法代替,或者将缩小原文件中字符的实际占用的比特位来减少了没用到的比特位。
就是将原文件中重复的内容用“长度距离对“进行替代,长度距离对是两个内容,即长度length,距离distance。距离表示,接下来 长度为length的内容和 和该字符前distance距离后的内容重复。
距离长度对 包含两个内容,每个内容占一个字节,也就是距离长度对压缩文件中占2个字节,所以只有当重复的内容大于等于3字节的时候,用长度距离对进行替换才能起到压缩的效果,相反如果遇到重复的单个字节就用长度距离对来替换,会使得文件越压缩越大。
1.首先明确,压缩过程就是把原文件中的重要的内容写到压缩文件中的过程,可以认为
是去重的过程,所以可以使用哈希表记录重复内容的信息。
2.在压缩过程中先读取原文件,将原文件内容按字节依次进行读取,如果是第一次出现
就将源字符写入压缩文件,并且将该字符串信息保存到哈希表中,方便后面进行内容对比,对比
是为了寻找最长的字符串长度,具体方法采用 闭散列 和 开散列 结合的方法。
3.寻找到最长字符串后,就可以将“长度距离对”写入压缩文件。
4.每次读一个字节,效率会比较低,所以每次读取一个缓冲区的内容,如果一次就能把所
有内容读完,就是小文件,否则就是大文件。二者的主要区别在于,用不用对缓冲区中
的滑动窗口中的数据进行搬移。
1.明确解压缩就是根据一个压缩文件,要完整的还原出原文件。原文件有后缀,有大小。
所以在上面的压缩文件中要把源文件的后缀和大小写进去。同时还有一个问题,写入
的长度距离对 如果和 源文件中的内容一样怎么办? 所以要进行区分,也就是压缩文
件的内容相当于双行道,既有源文件的实际内容(由未重复部分 和 最长重复部分组成)
也有用来区分写入的是 源字符 还是 标记的标记内容(因为只有两种状态,可以用比
特位进行区别,一个字节就相当于源文件中8个单位长度)。
2.具体的解压缩就是一个反其道行之的过程。从压缩文件中先取出标记文件的大小,再
得到源文件的大小,定位标记文件的位置,读取压缩内容的同时读取标记内容,如果
确定是字符信息就直接写到解压缩文件中,如果确定是长度距离对,就在已解压的内容
中查找重复的部分并进行还原。
按照其文件大小 分成小文件 和 大文件
具体的区别: 小文件压缩不用进行数据的搬移,相较简单,所以先分析小文件的压缩。
小文件压缩:
大文件压缩:
大文件压缩中 滑动窗口是如何确定的?
为什么要有最大匹配长度?
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 //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
#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
#include "Lz77.h"
#include
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;
}