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

前面基于Huffman算法和LZ77算法的文件压缩(四)
基于Huffman算法和LZ77算法的文件压缩(五)
已经充分讲解LZ77到基本原理和实现细节。

本文开始讲解文件的压缩过程。

一、回顾整个压缩过程

1.打开带压缩的文件(注意:必须按照二进制格式打开,因为用户进行压缩的文件不确定)
2.获取文件大小,如果文件大小小于3个字节,则不进行压缩
3.读取一个窗口的数据,即64K,
4.用前两个字符计算第一个字符与其后两个字符构成字符串哈希地址的一部分,因为哈希地址是通过三个字节算出来的,先用前两个字节算出一部分,在压缩时,再结合第三个字节算出第一个字符串完整的哈希地址。
5.循环开始压缩
a.计算哈希地址,将该字符串首字符在窗口中的位置插入到哈希桶中,并返回该桶的状态matchHead
b.根据matchHead检测是否找到匹配

  • 如果matchHead等于0,未找到匹配,表示该三个字符在前文中没有出现过,将该当前字符 作为源字符写到压缩文件中
  • 如果matchHead不等于0,表示找到匹配,matchHead代表匹配链的首地址,从哈希桶matchHead位置开始找最长匹配,找到后用该(距离,长度对)替换该字符串写到压缩文件中,然后将该替换串三个字符一组添加到哈希表中

6 . 如果窗口中的数据小于MIN_LOOKAHEAD时,将右窗口中数据搬移到左窗口,从文件中新读取一个窗口的数据放置到右窗,更新哈希表,继续压缩,直到压缩结束。

二、先封装一个哈希表

1.先给一个公共信息的文件common.h

#pragma once
#include <iostream>
#include <string>
#include <assert.h>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>//记录程序中用到的常量数据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 WSIZE = 32 * 1024;   //32k

2.哈希表的整体框架

#pragma once
#include "Common.hpp"class HashTable
{public:HashTable(USH size);~HashTable();//哈希表插入,//matchHead为出参,带出整个匹配链的头,ch为匹配的字符,pos为//在缓冲区当中的下标。hashAddr为哈希地址:出入输出型参数,//因为计算本次哈希地址需要用到上一个哈希地址void Insert(USH& matchHead, UCH ch, USH pos, USH& hashAddr);//哈希函数void HashFunc(USH& hashAddr, UCH ch);//获取哈希表前一个匹配头USH GetNext(USH matchHead);//在处理大于64k的大文件时,需要把右窗中的文件拷贝到左窗,需要更新哈希表void Update();private://哈希函数相关USH H_SHIFT();private:USH* prev_;USH* head_;};

3.哈希表的构造

HashTable::HashTable(USH size):prev_(new USH[size * 2])     //哈希表中存放的是索引字符串的首地址//(距离字符串开始的相对下标), head_(prev_ + size)
{memset(prev_, 0, size * 2 * sizeof(USH));//初始化为0,0也代表当前匹配是//链表的末尾(相当于用数组模拟的链表)
}

4.哈希表的析构函数

HashTable::~HashTable()
{delete[] prev_;prev_ = nullptr;
}

5.定义哈希函数相关变量

const USH HASH_BITS = 15;                  //哈希地址15位
const USH HASH_SIZE = (1 << HASH_BITS);    //哈希地址个数 32K
const USH HASH_MASK = HASH_SIZE - 1;       //防止溢出  低15位全1,因为prev的大小就WSIZE,而当start到达右窗
//时,下标明显大于WSIZE,如果不处理就会下标越界

6.哈希函数相关


//abcdefgh字符串
//hashaddr1:abc
//hashaddr2:bcd//hashAddr:前一次计算出的哈希地址  abc
//本次需要计算bcd哈希地址
//ch:本次匹配三个字符中的最后一个
//本次哈希地址是在上一次哈希地址的基础上计算出来的
void HashTable::HashFunc(USH& hashAddr, UCH ch)
{      //hashAddr是输入,输出型参数,ch是所查找字符串中第一个字符hashAddr = (((hashAddr) << H_SHIFT()) ^ (ch)) & HASH_MASK;
}USH HashTable::H_SHIFT()
{return (HASH_BITS + MIN_MATCH - 1) / MIN_MATCH;     //5
}

7.向哈希表中插入元素

//matchHead:匹配链的头
//ch:查找字符串的第三个字符(也就是最后一个)
//pos:查找字符串的头到字符串开始的距离
//hashAddr:输入时是上一次的哈希地址,输出时是本次哈希地址
void HashTable::Insert(USH& matchHead, UCH ch, USH pos, USH& hashAddr)
{HashFunc(hashAddr, ch);   //获取本次插入的哈希地址matchHead = head_[hashAddr];//找当前三个字符在查找缓冲区中找到的最近的个,//即匹配链的头,将来会用这个头来找匹配//将新的哈希地址插入链表//pos & HASH_MASK的目的是防止越界prev_[pos & HASH_MASK] = head_[hashAddr];head_[hashAddr] = pos;
}

三、开始压缩

1.整体框架

#pragma once
#include "HashTable.h"class LZ77
{public:LZ77();~LZ77();void CompressFile(const std::string& strFilePath);void UNCompressFile(const std::string& strFilePath);private://找最长匹配USH LongestMatch(USH matchHead, USH& curMatchDist, USH start);      //写标记文件void WriteFlag(FILE* fOUT, UCH& chFlag, UCH& bitCount, bool isLen); //合并压缩数据文件和标记信息文件void MergeFile(FILE* fOut, ULL fileSize);//在处理大于64k文件时,需要重新填充缓冲区的右窗口void FillWindow(FILE* fIn, size_t& lookAhead, USH& start);//获取文件的后缀std::string GetStr(std::string filename);private://用来保存待压缩数据的缓冲区即滑动窗口UCH* pWin_; //哈希表                                                        HashTable ht_;
};

2.构造函数

LZ77::LZ77():pWin_(new UCH[WSIZE * 2])//初始化缓冲区的大小,ht_(WSIZE)         //初始化hash表大小
{}

3.析构函数

LZ77::~LZ77()
{delete[] pWin_;pWin_ = nullptr;
}

3.根据MIN_LOCKAHEAD来过滤文件,如果文件大小小于MIN_LOCKAHEAD则不进行匹配

  • 注意文件的打开方式,和Huffman压缩的原理一样,为了保证压缩算法的通用性,既可以压缩文本文件也可以压缩二进制文件,需要以rb方式打开
  • 如何获取文件大小??结合fseek函数和ftell函数来获取文件大小
FILE* fIn = fopen(strFilePath.c_str(), "rb");if (nullptr == fIn) {std::cout << "打开文件失败" << std::endl;return;}//获取文件大小fseek(fIn, 0, SEEK_END);//把文件指针移动到文件的末尾ULL fileSize = ftell(fIn);//获取当前文件指针到文件开始位置的偏移量//1. 如果源文件的大小小于最小匹配长度 MIN_MATCH,则不进行处理,文件太小去进行压缩反而达不到压缩的目的if (fileSize <= MIN_MATCH) {          //此处是小于3个字符,在common.hpp文件中定义的std::cout << "文件太小,不压缩" << std::endl;return;}

4.从压缩文件中读取一个缓冲区的数据到窗口中

  • 注意在计算文件大小的时候已经把文件指针移动到文件的末尾,所以此处要把文件指针重新定位
 //从压缩文件中读取一个缓冲区的数据到窗口中//上面把文件指针移动到文件末尾,还得移回来,//否则读不到任何数据fseek(fIn, 0, SEEK_SET);//表示先行缓冲区中剩余的字节数size_t lookAhead = fread(pWin_, 1, 2 * WSIZE, fIn);

5 .先计算最开始两个字符的哈希地址

  • 因为哈希地址是根据前面的字符的哈希地址求的,所以要先计算前两个字符的哈希地址。如abcdefg……,那么满3个字符才可以进行匹配,所以需要先计算ab的哈希地址才可以计算c的哈希地址,然后进行匹配
USH hashAddr = 0;//记录哈希地址//设置起始hashAddr (前两个字符的hash地址)for (USH i = 0; i < MIN_MATCH - 1; ++i) {ht_.HashFunc(hashAddr,pWin_[i]);}

6.循环进行压缩

  • 注意匹配链头matchHead的含义。如果为0,代表没找到匹配,因为哈希表初始化的时候都为0
  • 那么如果找到匹配,就顺着匹配链往下找最长匹配
  • 封装一个LongestMatch函数来进行查找最长匹配
  • 在找最长匹配LongestMatch的时候需要获得当前匹配链的下一个匹配头
//获取当前匹配头的下一个匹配头(如果为0代表匹配结束)
USH HashTable::GetNext(USH matchHead)
{return prev_[matchHead & HASH_MASK];
}
  • 找到最长匹配就需要返回长度距离信息
//在找的过程中,需要将每次找到的匹配结果进行比较,保持最长匹配
USH LZ77::LongestMatch(USH matchHead, USH& MatchDist, USH start)
{     //找最长匹配USH curMatchLen = 0;  //一次匹配的长度USH maxMatchLen = 0;UCH maxMatchCount = 255;   //最大的匹配次数,解决环状链USH curMatchStart = 0;     //当前匹配在查找缓冲区中的起始位置//在先行缓冲区中查找匹配时,不能太远即不能超过MAX_DISTUSH 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 > maxMatchLen) {maxMatchLen = curMatchLen;//记录当前匹配的开始位置,方便后面计算匹配距离curMatchStart = matchHead;}} while ((matchHead = ht_.GetNext(matchHead)) > limit&& maxMatchCount--);MatchDist = start - curMatchStart;return maxMatchLen;
}
  • while ((matchHead = ht_.GetNext(matchHead)) > limit && maxMatchCount--):matchHead代表下一个匹配头即在缓冲区当中的下标。

  • USH limit = start > MAX_DIST ? start - MAX_DIST : 0;代表如果start > MAX_DIST就从start - MAX_DIST找匹配头,否则就从开始找匹配头

  • matchHead = ht_.GetNext(matchHead)) > limit代表在limit的范围内进行找匹配,太远就不进行匹配

  • maxMatchCount--是为了解决&WMASK带来死循环的问题及太远不进行匹配

  • 执行找最长匹配后,需要验证是否成功找到匹配,因为matchHead可能为0,就没有执行找最长匹配函数

  • 前面我们说过最大匹配长度为[0,258],如果用一个字节来记录是有问题的,需要用两个字节来保存,而写入文件的时候又不能写入两个字节,所以在写入长度的时候临时定义一个字节变量保存原来用两个字节来保存长度的变量-3,最后把一个字节的临时变量写入当压缩文件当中,这样就解决问题了

  • 如果没找到匹配就需要把当前字符写入到压缩文件当中,如果找到最长匹配,就需要写入长度距离信息到压缩文件当中(注意写长度的时候要把长度-3),但是压缩数据和长度距离对信息无法区分

  • 所以还要写入标记信息用0标记原字符,1标记长度(距离不用标记,下两个字节就是,)

  • 但是标记信息和压缩数据应该保存在不同的文件当中,无法一边写压缩数据一边写标记信息,那样无法区分

//chFlag:该字节中的每个比特位是用来区分当前字节是原字符还是长度?
//0:原字符
//1:长度
//bitCount:该字节中的多少个比特位已经被设置
//isCharOrLen:代表该字节是源字符还是长度,判断压缩数据还是长度信息
void LZ77::WriteFlag(FILE* fOUT, UCH& chFlag, UCH& bitCount, bool isLen)
{chFlag <<= 1;if (isLen)//如果是长度,就给当前的比特位置为1chFlag |= 1;bitCount++;if (bitCount == 8) {//代表chFlag8位已经设置完,将该标记写入到压缩文件中fputc(chFlag, fOUT);//重新开始chFlag = 0;bitCount = 0;}
}
  • 注意如果找到匹配,在执行写入长度距离信息后,还需要把匹配的字符写入到哈希表当中,否则无法进行下一次匹配(匹配的长度那么多的字符是不进行下一次匹配的,因为已经被替换掉),如abcdefg,如果abc找到匹配,把长度距离信息写完后还要把bcd、cde写入到哈希表当中,下一次直接从d开始找匹配,而又因为abc在之前插入的时候已经写过了,此处就不用写。

7.压缩文件完整代码

//压缩文件
void LZ77 :: CompressFile(const std::string& strFilePath)
{FILE* fIn = fopen(strFilePath.c_str(), "rb");if (nullptr == fIn) {std::cout << "打开文件失败" << std::endl;return;}//获取文件大小fseek(fIn, 0, SEEK_END);//把文件指针移动到文件的末尾ULL fileSize = ftell(fIn);//获取当前文件指针到文件开始位置的偏移量//1. 如果源文件的大小小于最小匹配长度 MIN_MATCH,则不进行处理,文件太小去进行压缩反而达不到压缩的目的if (fileSize <= MIN_MATCH) {          //此处是小于3个字符,在common.hpp文件中定义的std::cout << "文件太小,不压缩" << std::endl;return;}//从压缩文件中读取一个缓冲区的数据到窗口中fseek(fIn, 0, SEEK_SET);//上面把文件指针移动到文件末尾,还得移回来,否则读不到任何数据size_t lookAhead = fread(pWin_, 1, 2 * WSIZE, fIn);//表示先行缓冲区中剩余的字节数USH hashAddr = 0;//记录哈希地址//设置起始hashAddr (前两个字符的hash地址)for (USH i = 0; i < MIN_MATCH - 1; ++i) {ht_.HashFunc(hashAddr,pWin_[i]);}//开始写压缩数据://根据获取的文件后缀打开一个同样文件后缀的压缩数据存储文件//std::string str = GetStr(strFilePath);//FILE* ffIn = fopen("5.txt","wb");//fwrite(str.c_str(),sizeof(str),1,ffIn);//fclose(ffIn);//压缩FILE* fOUT = fopen("2.lzp", "wb");assert(fOUT);USH start = 0;//查找字符串在缓冲区的地址,随着压缩的不断进行,start不断的在先行缓冲区中增加//与查找最长匹配相关的变量USH matchHead = 0;       //匹配字符串的头USH curMatchLength = 0;  //一次匹配字符串的长度USH curMatchDist = 0;    //一次匹配字符串的距离//与写标记相关的变量UCH chFlag = 0;//代表当前字符是字符还是长度UCH bitCount = 0;//代表1个字节已经用了多少字节//写标记的文件FILE* fOutF = fopen("3.txt", "wb");assert(fOutF);//lookAhead表示先行缓冲区中剩余字节的个数while (lookAhead) {//1.将第三个字符插入到哈希表中,因为前两个字符已经插入到哈希表当中,//(pWin_[start],pWin_[satrt + 1].pWin_[start + 2])并获取匹配链的头ht_.Insert(matchHead, pWin_[start + 2], start, hashAddr);//因为不只进行一此匹配,每次匹配前都要置为0,防止影响后面的数据curMatchLength = 0;curMatchDist = 0;//2.验证在查找缓冲区中是否找到匹配,如果有匹配,找最长匹配//因为在初始化哈希表的时候都设置为0,代表没有任何匹配,如果不为0,代表有匹配if (matchHead) {//顺着匹配链找最长匹配,最终带出<长度,距离>对curMatchLength = LongestMatch(matchHead, curMatchDist, start);}//3.验证是否找到匹配if (curMatchLength < MIN_MATCH) {//在查找缓冲区中未找到重复字符串//将start位置的字符写入到压缩文件中fputc(pWin_[start], fOUT); //写字符//写当前原字符对应的标记WriteFlag(fOutF, chFlag, bitCount, false);//写标记//更新变量++ start;lookAhead--;}else {//找到匹配//将《长度,距离》对写入压缩文件中//写长度UCH chLen = curMatchLength - 3;//因为长度是1个字节,其范围本来是//[0,255],但是因为是3个一组,所以其范围是[3,258]fputc(chLen, fOUT);//写距离fwrite(&curMatchDist, sizeof(curMatchDist), 1, fOUT);//写当前原字符对应的标记WriteFlag(fOutF, chFlag, bitCount, true);//更新先行缓冲区中剩余的字节数lookAhead -= curMatchLength;//将已经匹配的字符串按照三个一组将其插入到哈希表中--curMatchLength;  //当前字符串已经插入++start;while (curMatchLength) {ht_.Insert(matchHead, pWin_[start + 2], start, hashAddr);--curMatchLength;++start;}}//检测先行缓冲区中剩余字符个数,如果小于最短匹配长度,//需要更新缓冲区和哈希表,向缓冲区中重新填充内容,if (lookAhead <= MIN_LOOKAHEAD)FillWindow(fIn, lookAhead, start);}//标记位数如果不够八位,因为写标记是8位写一次,最后可能//不够8bit,所以要另外判断if (bitCount > 0 && bitCount < 8) {chFlag <<= (8 - bitCount);fputc(chFlag, fOutF);}fclose(fOutF);//把压缩数据文件和标记信息文件合并MergeFile(fOUT, fileSize);//删除标记信息文件remove("./3.txt");fclose(fIn);fclose(fOUT);
}

到这里文件压缩过程基本结束了,还有大文件处理方式在后面解决

基于LZ77算法的文件压缩收尾相关推荐

  1. 基于LZ77算法的文件压缩铺垫

    基于Huffman算法和LZ77算法的文件压缩(四) 本文开始讲解LZ77算法,会用到哈希,哈希原理详解 我们在基于Huffman算法和LZ77算法的文件压缩(一)当中总体介绍了Huffman算法和L ...

  2. 基于LZ77算法的文件压缩

    基于Huffman算法和LZ77算法的文件压缩(五) 基于Huffman算法和LZ77算法的文件压缩(四)已经讲解LZ77算法到基本原理和压缩过程. 本文详细讲解文件压缩过程当中的问题 一.文件压缩的 ...

  3. 基于LZ77算法的文件解压缩项目缺陷分析

    基于Huffman算法和LZ77算法的文件压缩(七) 基于Huffman算法和LZ77算法的文件压缩(六)已经讲解完文件压缩的过程,本文讲解文件解压缩的过程和大文件处理方式 一.解压缩的流程 LZ77 ...

  4. 基于Huffman算法和LZ77算法的文件压缩的改进方向

    基于Huffman算法和LZ77算法的文件压缩(八) 到这里已经简单实现基于Huffman算法和LZ77算法的文件压缩, GitHub源码:点我 根据基于Huffman算法和LZ77算法的文件压缩(七 ...

  5. 基于Huffman算法实现文件压缩解压缩(C语言)

    一.实现步骤 统计源文件中字符种类和频率 建立Huffman编码树 生成Huffman编码表 压缩文件时,字符匹配编码,将编码写入压缩后文件 解压缩文件时,读取编码,匹配编码表中的字符,写入解压缩后的 ...

  6. 基于Huffman算法的文件解压缩

    基于Huffman算法和LZ77算法的文件压缩(三) 基于Huffman算法和LZ77算法的文件压缩(一)和 基于Huffman算法和LZ77算法的文件压缩(二)讲解Huffman压缩的基本原理和文件 ...

  7. 哈夫曼字符串编码c语言实现,基于哈夫曼(haffuman)算法的文件压缩的实现(C语言)(原创)...

    本文首先简要阐述哈夫曼算法的基本思想,然后介绍了使用哈夫曼算法进行文件压缩和解压缩的 处理步骤,最后给出了C语言实现的文件压缩和解压缩的源代码. 哈夫曼算法的主要思想是: ①首先遍历要处理的字符串,得 ...

  8. 基于哈夫曼算法的文件压缩软件

    数据结构课设(一) 作业要求 1.设计并实现一个使用哈夫曼算法对文件进行压缩的工具软件. 2.通过命令行参数指定操作模式(压缩/解压).源文件名.目标文件名. 3.压缩操作将源文件按字节读入并统计字节 ...

  9. 单向散列函数概述并基于MD5算法对文件哈希值实时监测

    1.如何验证文件是否被修改过 只生成一个指纹文件,对指纹文件进行验证 当已经存储的文件被修改之后,指纹文件就会跟着变化,即生成一个单向散列函数 任意长度的数据都对应固定长度的散列值–减少匹配开销 散列 ...

最新文章

  1. sql server 2008 故障转移群集
  2. python真的那么火吗-现在为什么 Python 这么火?
  3. pdf.js 在线阅读PDF
  4. AliOS Things 硬件抽象层(HAL)对接系列2 — SPI driver porting
  5. [iPhone高级] 基于XMPP的IOS聊天客户端程序(IOS端二)
  6. 基于LiteOS Studio零成本学习LiteOS物联网操作系统
  7. 过滤SQL关键字 防注入
  8. P1018 乘积最大(高精度加/乘)
  9. java虚拟机的gc机制的优缺点_深入Java虚拟机之 -- 总结面试篇
  10. 增量式PID计算公式4个疑问与理解
  11. python-2.找出数组中重复的数字
  12. Linux基础知识(5)-压缩与关机命令
  13. 软路由Linux7,CentOS 7 NAT软路由
  14. JDE910笔记2--OMW项目建立及简单使用[转]
  15. DAVINCI DM365-DM368开发攻略—U-boot-2010.12-rc2-psp03.01.01.39及UBL的移植
  16. train_test_split参数含义
  17. NLP实战|如何用280多万条豆瓣影评预测电影评分?
  18. OOM异常的发生原因
  19. 如何设计神经网络结构图,神经网络设计与实现
  20. 计算机如何配置桌面显示属性,WindowsXP显示属性的设置教程

热门文章

  1. Mysql-高性能索引
  2. 跟我学交换机配置(一)
  3. 工作流学习——Activiti整体认识二步曲
  4. 《用友ERP-U8(8.72版)标准财务模拟实训》——导读
  5. 数据库索引的实现原理及查询优化
  6. ConnectivityManager ConnectivityService in Android
  7. 多数据库支持的应用程序设计(来自深空老大)
  8. CodeForces - 1288C Two Arrays(组合数学)
  9. 中石油训练赛 - 招待(思维)
  10. kubect安装 windows_kubectl工具的windows安装方法