1、文件压缩
1、什么是文件压缩?
在不丢失有用信息的前提下,缩减数据量以减少存储空间,提高其传输、存储和
处理效率,或按照一定的算法对数据进行重新组织,减少数据的冗余和存储的空 间
的一种技术方法。通俗来说,就是想办法,让文件变得更小(可以还原)
2、问什么要对文件进行压缩?
文件太大,节省空间
提高数据在网络上的传输效率
对数据保护---加密
3、文件压缩分类
无损压缩
源文件被压缩后,通过解压缩,可以还原为与源文件相同格式。
有损压缩
源文件被压缩后,通过解压缩,不能还原为与源文件相同格式。
解压缩文件时,对识别文件内容无影响。
2、Huffman压缩的引入
字节层面可能也有大量重复的。比如:"BCDCDDBDDCADCBDC"
一个字节占8个比特位,那如果能对所有字节找到小于8个比特位的编码,
然后用找到的编码对源文件中对应。
思考:字节重新进行改写,也可以让源文件更小。那如何找编码呢?
1、静态等长编码
每个字符的编码长度都相等,比如:
用等长编码对上述源数据进行压缩:01101110 11110111 11100011 10011110,
压缩完成后的结果只占4个字节,压缩率还是比较高的。
2、动态不等长编码
每个字符的编码根据具体的字符情况来确定,比如:
使用不等长编码对源数据进行压缩:10111011 00101001 11000111 01011
压缩完成后最后一个字节没有用完,还剩余3个比特位,
显然动态不等长编码比等长编码压缩率能好点。
问题那动态不等长编码如何获取到呢?
3、 huffman编码
1. haffman树
从二叉树的根结点到二叉树中所有叶结点的路径长度与相应权值的乘积之和
为该二叉树的带权路径长度WPL。
把带权路径最小的二叉树称为Huffman树。
2、huffman的创建
-
由给定的n个权值{ w1, w2, w3, … , wn}构造n棵只有根节点的二叉树森林F={T1, T2 , T3, … ,Tn},
每棵二叉树Ti只有一个带权值wi的根节点,左右孩子均为空。 -
重复以下步骤,直到F中只剩下一棵树为止
在F中选取两棵根节点权值最小的二叉树,作为左右子树构造一棵新的二叉树,新二叉树根节点的权值为其左右子树根节点的权值之和在F中删除这两棵二叉树把新的二叉树加入到F中。
3. 获取haffman编码
1、每个字符出现的总次数为权值,构建Huffman树
2、对Huffman树左分支用0代替,右分支用1代替
3、所有权值节点都在叶子位置,遍历每条叶子节点的路径,获取字符编码
A:100
B:101
C:11
D:0
因为Huffman树权值节点都在叶子结点的位置,路径唯一,
从而保证了在字符压缩时,不会出现冲突。
问题:如何找到权值最小的构建二叉树呢?
(1) 排序 (可以实现,效率太低)
采用vector保存,权值最小的排在前面。
Huffman树的构建过程相当于头删
(2)采用堆(priority_queue)保存
4、利用huffman编码对源文件进行压缩
1. 统计源文件中每个字符出现的次数
2. 以字符出现的次数为权值创建huffman树
3. 通过huffman树获取每个字符对应的huffman编码
4. 读取源文件,对源文件中的每个字符使用获取的huffman编码进行改写,
将改写结果写到压缩文件中,直到文件结束
5、 解压缩
- 从压缩文件中获取源文件的后缀
- 从压缩文件中获取字符次数的总行数
- 获取每个字符出现的次数
- 重建huffman树
- 解压缩
Huffman.hpp
#pragma once
#pragma warning(disable:4996)
#include <iostream>
//using namespace std;
#include <queue>
#include <vector>
template<class W> //W 代表节点的权值类型
class HuffManTreeNode
{
public:
HuffManTreeNode(const W& weight =W()) // 构造,创建HuffMan结点
:_pLeft(nullptr)
,_pRight(nullptr)
, _pParent(nullptr)
, _weight(weight)
{
}
HuffManTreeNode<W>* _pLeft; // 左指针
HuffManTreeNode<W>* _pRight;// 右指针
HuffManTreeNode<W>* _pParent;// 双亲指针
W _weight; // 权值
};
template <class W>
class Greater
{
typedef HuffManTreeNode<W> Node;
public:
Greater()
{}
bool operator()(const Node* pLeft, const Node* pRight)
{
return pLeft->_weight > pRight->_weight; // 自定义类型需要重载
}
};
template<class W>
class HuffmanTree // 创建HuffMan树
{
typedef HuffManTreeNode<W> Node; // 重命名
public:
HuffmanTree()
:_pRoot(nullptr)
{
}
HuffmanTree(const std::vector<W> vweight, const W& invalid_weight) // 传入权值数组
:_pRoot(nullptr)
{
CreateHuffManTree(vweight, invalid_weight);
}
~HuffmanTree()
{
_DestroyTree(_pRoot); // 销毁Huffman树
}
void CreateHuffManTree(const std::vector<W>& vWeight ,const W& invalid_weight) // 传入 权值, 无效权值invalid_weight , 过滤传入权值为0得
{
// (1). 构建森林
// 1、采用优先级队列,存放结点,而优先级队列,只需要存放这些结点的地址就行
std::priority_queue<Node*, std::vector<Node*>, Greater<W>> pq; // 小堆
for (auto e : vWeight) //
{
if (e == invalid_weight) // 若传入为无效权值,跳过
continue;
//Node(e)将权值存入结点,new 返回地址,将结点地址压入优先队列
pq.push(new Node(e)); //优先对列,存放节点的地址
}
while (pq.size()>1) //优先队列中存放超过1个结点地址
{
Node* pLeft = pq.top(); // 堆顶最小权值结点地址
pq.pop();
Node* pRight = pq.top();// 优先队列更新后,堆顶最小权值结点地址
pq.pop();
// 将 用所取出两个结点地址,获取权值,
// 创建新的结点,将两个结点的权值之和,存在在 新结点中
Node* pParent = new Node(pLeft->_weight + pRight->_weight); // 注意:_weight 得 类型为 W ,可以时自定义类型或者内置类型
// 并将新节点地址作为双亲结点,
pParent->_pLeft = pLeft;
pParent->_pRight = pRight;
pLeft->_pParent = pParent; // 左子树的双亲
pRight->_pParent = pParent;// 右子树的双亲
// 将新地址节点压入
pq.push(pParent);
}
//跳出while 循环,此时就只剩一棵树 , 即 Huffman树 -- 构建成功
_pRoot = pq.top();
}
Node* GetRoot()
{
return _pRoot;
}
void _DestroyTree(Node*& pRoot)//pRoot 为 _pRoot堆顶 的一份拷贝,因此需要用二级指针 或者 引用 才能将 栈顶_pRoot置空
{
if (pRoot) // 加入堆顶指针不为空,存在
{
// 递归销毁
_DestroyTree(pRoot->_pLeft); //先消除左子树
_DestroyTree(pRoot->_pRight); //在消除右子树
delete pRoot;
pRoot = nullptr;
}
}
private:
Node* _pRoot; //堆顶
};
Huffman.hpp
#pragma once
#pragma warning(disable:4996)
// 基于Huffman的压缩
#include <string>
#include <assert.h>
#include <algorithm>
#include <functional>
#include "Huffman.hpp"
// 统计源文件字符出现的次数
struct CharInfo
{
unsigned char _ch; // 具体的字符
size_t _count; // 字符出现的次数
std::string _strCode; // 字符的Huffman编码
CharInfo(size_t count = 0)
:_count(count)
{
}
CharInfo operator+(const CharInfo& ch)const // 重载两个对象相加 , 返回值也必须为对象
{
// 不能返回return ch._count + _count;
//
return CharInfo(ch._count + _count); // 返回一个无名结构体对象
}
bool operator>(const CharInfo& ch)const
{
return _count >ch._count; // 返回一个无名结构体对象
}
bool operator==(const CharInfo& ch)const //
{
return _count == ch._count; // 只需要判断次数,过滤掉次数为0的
}
};
class FileCompressHuffman // 基于Huffman的压缩
{
public:
FileCompressHuffman(); // 初始化统计字符数组
void CompressFile(const std::string& path); //压缩文件的路径
void UNCompressFile(const std::string& path); //解压文件存放路径
private:
void GenerateHuffmanCode(HuffManTreeNode<CharInfo>* pRoot);// 获取每个字符的编码
void WriteHead(FILE* fOut, const std::string& fileName); // 对压缩文件添加头部信息(文件后缀,编码行数,字符次数) ,filePostFix 文件后缀
std::string GetFilePostFix(const std::string& fileName); //获取文件名
void ReadLine(FILE* fIn, std::string& strInfo); // 读取一行字符串
private:
std::vector<CharInfo> _fileInfo; // 结构体数组 统计保存 下标对应 字符种类,及各种字符数量
};
Huffman.cpp
#include "FileCompressHuff.h"
FileCompressHuffman::FileCompressHuffman() // 构造函数,初始化统计字符数组
{
_fileInfo.resize(256);
for (int i = 0; i < 256; ++i)
{
_fileInfo[i]._ch = i;
_fileInfo[i]._count = 0;
}
}
void FileCompressHuffman::CompressFile(const std::string& path)//压缩文件路径
{
// 1.统计源文件字符出现的次数
// 1.1 打开文件
FILE* fIn = fopen(path.c_str(),"rb");//
if (nullptr == fIn)
{
assert(false);// 文件打开失败
return;
}
//1.2 文件打开成功
//注意 char -1289 ---> 127 无法保存 256个字符
unsigned char* pReadBuff = new unsigned char[1024]; //开辟1024字节缓存区,供每次读取1k数据
// 读取数据保存至pReadBuff,一个字符1字节,每次读取1024个,从fIn文件指针中读取;
int readSize = 0;
while (true)
{
readSize = fread(pReadBuff, 1, 1024, fIn);
if (0 == readSize) // 如果读取到的字符为0,则已经读完了
break;
for (int i = 0; i < readSize; ++i)
{
//pReadBuff[i] --- 缓冲区第i个字符
_fileInfo[pReadBuff[i]]._count++; // 缓冲区第i个字符,对应得下标
}
}
fseek(fIn, 0, SEEK_SET); // 将fIn文件指针,移到起始读的位置,后面改写
//2、以字符出现的次数为权值创建huffman树
HuffmanTree<CharInfo> t(_fileInfo, CharInfo()); //_fileInfo 记录字符数量得 vector
//3、获取每个字符的编码
GenerateHuffmanCode(t.GetRoot());
//4、用获取到的编码重新改写源文件
FILE* fOut = fopen("2.txt","wb"); // 以只写形式,写出改写后的文件
if (nullptr == fOut)
{
assert(false);
return;
}
// 4.1 先向文件中添加 文件头 (文件名 , 行数 ,次数)等信息
WriteHead(fOut,path); //path文件路径,
// 4.2 添加编码
char ch = 0; // 替换变量
int bitCount = 0; // 比特位计数
while (true)
{
readSize = fread(pReadBuff,1,1024,fIn);
if ( 0 == readSize)
{
break;
}
//4.1 根据字符编码对读取到的内容进行重写 , 用Huffman编码进行字符替换
for (size_t i = 0; i < readSize; ++i)
{
std::string strCode = _fileInfo[pReadBuff[i]]._strCode ; // 用Huffman编码进行字符替换
for (size_t j = 0; j < strCode.size(); ++j)
{
ch <<= 1; // 只有 ++ -- 改变原变量
if ('1' == strCode[j])
ch |= 1;
bitCount++;
if (8 == bitCount) // 最后一次,可能不够8个比特位
{
fputc(ch, fOut); //将 字节 写入fOu->文件
bitCount = 0;
ch = 0;
}
}
}
}
if (bitCount < 8)
{
ch << (8 - bitCount); //因为是左移,所有不足8位,要将后面的往前移到第一个bit位
fputc(ch,fOut);
}
delete[] pReadBuff;
fclose(fIn);
fclose(fOut);
}
void FileCompressHuffman::GenerateHuffmanCode(HuffManTreeNode<CharInfo>* pRoot)
{
if (pRoot == nullptr)
return;
GenerateHuffmanCode(pRoot->_pLeft);// 递归到叶子节点
GenerateHuffmanCode(pRoot->_pRight);
if (nullptr == pRoot->_pLeft && nullptr == pRoot->_pRight) // 叶子结点
{
std::string strCode;
HuffManTreeNode<CharInfo>* pCur = pRoot; // 保存叶子节点
HuffManTreeNode<CharInfo>* pParent = pRoot->_pParent; // 保存叶子节点双亲
while (pParent)
{
if (pParent->_pLeft == pCur)
{
strCode += '0'; //
}
else
{
strCode += '1';
}
pCur = pParent;
pParent = pCur->_pParent;
}
reverse(strCode.begin(),strCode.end());
_fileInfo[pRoot->_weight._ch]._strCode = strCode;
}
}
// 2.txt
//f:\123\2.txt
std::string FileCompressHuffman::GetFilePostFix(const std::string& fileName) //获取文件名
{
return fileName.substr(fileName.rfind('.')); // 截取文件名 .文件类型
}
void FileCompressHuffman::WriteHead(FILE* fOut, const std::string& fileName) 对压缩文件添加头部信息(文件后缀,编码行数,字符次数)
{
assert(fOut);
std::string strHead;
strHead += GetFilePostFix( fileName);; // (1)向文件头添加后缀
strHead += '\n';
// 1、写文件的后缀
//fwrite(filePostFix.c_str(), 1, filePostFix.size(), fOut); //将 filePostFix.c_str() 通过fOut写入文件,每次写1字节
// 写行数
size_t lineCount = 0; // huffman编码的行数,// 统计待写入的 行数
std::string strChCount; //记录各个字符及出现的次数,换行信息,// 统计待写入的 次数
char szValue[32] = { 0 };
for (int i = 0; i < 256;++i) // 统计每个出现不为0次数的字符,及其次数
{
CharInfo& charInfo = _fileInfo[i];
if (_fileInfo[i]._count) //_fileInfo[i]._count 为 size_t类型
{
lineCount++; // 统计待写入的 行数
strChCount += charInfo._ch; // 对应字符
strChCount += ':'; // 字符与字符次数之间用冒号区分
//itoa(charInfo._count,szValue,10); // 将charInfo._count转化为字符类型,存入字符串strChCount中
strChCount += _itoa(charInfo._count, szValue, 10); // 字符出现次数 , 需要将size_t 转化为 string ,存入字符串中
strChCount += '\n'; // 换行
}
}
//2、 将行数转化为 字符型 , 写入行数
// itoa(lineCount,szValue,10); // 将行数转化为 字符型
strHead += _itoa(lineCount, szValue, 10); // (2)向文件头 添加 行数
strHead += '\n';
strHead += strChCount; //(3) 向文件头添加 字符次数信息
//将头写入文件
fwrite(strHead.c_str() , 1 , strHead.size(),fOut);
// 写入字符次数信息
}
//解压文件
void FileCompressHuffman::ReadLine(FILE* fIn, std::string& strInfo) // 读取一行字符串
{
assert(fIn);
while (!feof(fIn)) // 只要fIn指针没有到文件末尾
{
char ch = fgetc(fIn);//文件中读取一个字符,ch接收
if (ch == '\n')// \n 为一行
{
break;
}
strInfo += ch;
}
}
void FileCompressHuffman::UNCompressFile(const std::string& path) //解压文件存放路径
{
FILE* fIn = fopen(path.c_str(), "rb"); // 以读的方式打开压缩文件
if (nullptr == fIn)
{
assert(false);
return;
}
///1、 文件头部
/// 1.1 文件后缀
std::string strFilePostFix; // 保存文件后缀
ReadLine(fIn, strFilePostFix); // 通过一行一行的读取头部信息 , 不能使用getline() --- 文件流对象,不能使用文件指针;
// 1.2字符信息的总行数
std::string strCount;
ReadLine(fIn,strCount);
size_t lineCount = atoi(strCount.c_str()); // 将字符转换为数字
//1.3字符信息
// A:1
// B:3
// C:5
// D:7
for (int i = 0; i < lineCount; ++i)
{
std::string strchCount; // 字符信息
ReadLine(fIn, strchCount); // 读取一行字符信息
if (strchCount.empty())
{
strchCount += '\n';
ReadLine(fIn,strchCount);
}
// A:1
// B:3
// C:5
// D:7
//_fileInfo[strchCount[0]]; // strchCount[0] --- 字符种类
//strchCount需要选转化为c字符; // strchCount.c_str()[2]--- 字符对应出现数目 -- 需要转化为整型
_fileInfo[(unsigned char)strchCount[0]]._count = atoi(strchCount.c_str() + 2);
//char testAA = _fileInfo[strchCount[0]]._ch;
//int testCount = _fileInfo[strchCount[0]]._count;
}
// 还原Huffman树
HuffmanTree<CharInfo> t;
t.CreateHuffManTree(_fileInfo,CharInfo());
FILE* fOut = fopen("3.txt","w"); // 写
assert(fOut);
///2、对压缩后的Huffman还原
char* pReadBuff = new char[1024];
unsigned char ch = 0;
HuffManTreeNode<CharInfo>* pCur = t.GetRoot();
size_t fileSize = pCur->_weight._count; // 文件大小
size_t unCount = 0;// 解压字符数
while (true)
{
size_t readSize = fread(pReadBuff,1,1024,fIn);
if (0 == readSize)
{
break;
}
for (size_t i = 0; i < readSize; ++i)
{
ch = pReadBuff[i];
// 只需要将一个字节中的8个比特位单独处理
for (int pos = 0; pos < 8; ++pos)
{
if (nullptr == pCur->_pLeft && nullptr == pCur->_pRight) // 到达叶子节点
{
//unCount++;
fputc(pCur->_weight._ch, fOut);// 将字符写入到fOut所指的文件中
if (unCount == fileSize)
break;
pCur = t.GetRoot(); // pCur 回到根的位置
}
//80 ----二进制 100000000
if (ch & 0X80) // 如果为 1 , 右子树
{
pCur = pCur->_pRight;
}
else // 如果为 0 , 左子树
{
pCur = pCur->_pLeft;
}
ch <<= 1; // 每次左移一位
if (nullptr == pCur->_pLeft && nullptr == pCur->_pRight) // 到达叶子节点
{
unCount++;
fputc(pCur->_weight._ch, fOut);// 将字符写入到fOut所指的文件中
if (unCount == fileSize)
break;
pCur = t.GetRoot(); // pCur 回到根的位置
}
}
}
}
delete[] pReadBuff;
fclose(fIn);
fclose(fOut);
}