什么是HuffMan压缩:
简单来说就是利用Huffman树生成Huffman编码,对文件重复出现的字符进行记录,以减少出现次数。从而达到压缩文件的目的。
为什么HuffMan就能实现文件压缩呢?
数据在硬盘中的存储是有格式的,比如说字符就是char类型的,占了8个比特位,但是实际上有些字符可能根本就用不了8个比特位,因此就造成了空间的浪费。而huffman就是根据字符出现的次数重新建立存储规则,减少这些空间浪费。
什么是HuffMan树?
定义: Huffman树,又称最优二叉树,是加权路径长度最短的二叉树。
带权路径长度 = 节点的权值 * 当前节点距离根节点的路径长度
如何构建HuffMan树?
生成HuffMan编码
HuffMan树中左子树路径标记为0,右子树路径标记为1.从根节点到叶子结点的编码就是该字符的Huffman编码。
HuffMan文件压缩原理:
1.统计待压缩文件中每个字符出现的次数
- 构建结构体,存放信息:字符,次数,编码
- 遍历源文件,将每个字符的次数写入对应的结构体信息中
2.将字符出现的次数作为权值构建Huffman树
- 普通的二叉树只能通过双亲节点找到子节点,但是在获取编码的时候需要通过叶子结点往根节点走,所有构建二叉树时,要有子节点指向双亲节点
3.通过Huffman树获取每个字符所对应的编码
- 通过叶子结点找根节点的编码是逆序的,所以要使用reverse
4.向压缩文件中写入信息
- 压缩文件中只保存压缩的文件内容是不够的,还要有一些记录源文件内容的信息
- 第一行:存放源文件的后缀,因为在解压缩文件的时候需要后缀
- 第二行:存放源文件字符,次数 的总行数
- 第三行:存放字符和字符出现的次数–为了解压缩重建Huffman树
- 剩余内容是压缩编码
例如:源文件名为“1.txt”,存放“ABBBCCCCCDDDDDDD”
在压缩文件中的模型为:
解压缩:
1.获取后缀
2.获取字符以及对应的次数
3.重建Huffman树
4.解压压缩数据
- 从压缩文件中读取一个字节ch
- 从根节点开始,按照ch的8个比特位信息(代表字符的编码)从高到低遍历Huffman树:
a.该比特位是0,取当前节点的左孩子,否则取右孩子
b.一直遍历,直到遍历到叶子节点位置,该字符就被解析成功,讲解压出来的字符写入文件
c.如果在遍历Huffman过程中,8个比特位已经比较完毕还没有到达叶子节点,就从第四步开始执行
d.重复以上过程,直到所有的数据解析完毕
问题:
1.压缩汉字时候程序会崩溃,但是压缩字母程序就能正常运行?
- 原因:创建存放信息的数据是char 类型的,类型大小是1字节(0~255),但是汉字都是占两个字节的,对应的char类型就会因为超过255而导致数组的下边变成负数,数组下边越界访问导致程序崩溃。
- 解决方法:将存放信息的数据类型改成unsigned char 类型
2.解压大文件,只能解压缩一部分内容?
- 原因:一般情况下,文件指针碰到EOF就表示到文件结尾了,因为EOF是 -1,也就是FF,所以只解压了一部分(解压到第一个FF就停止了)。
- 解决方法:采用 feof()函数,多加一个判断即可,feof()函数就是判断文件末尾,而不仅仅是碰见EOF停止。
3.为什么压缩照片的时候会失败?
- 原因:是因为 ‘\0’ 的问题,因为如果刚开始把(字节,次数)先写入 buf 中,再由 buf 通过 fwrite 函数写入文件中,一定会出现问题(可能出现 0 字节)。因为 fwrite 的第一个参数要求C格式的字符串,把 buf 转化为C格式的字符串,如果遇见 ‘\0’,就会停止,所以就会崩溃。
- 解决方法:不要将字节写入 buf 中,而是通过 fputc 直接把字节写入文件中,然后再写入 buf 中,再将buf写入文件即可。
文件压缩的图解过程:
源码:
main.c
#include "HuffmanTree.hpp"
#include "FileCompressHuffman.h"
int main()
{
//TestHuffman();
FileCompressHuffmanM test;
//test.CompressFile("文件压缩(原文件).png");
test.UnCompressFile("文件压缩.hzp");
return 0;
}
huffmanTree.hpp
#pragma once
#include<iostream>
using namespace std;
#include<vector>
#include<queue>
template<class W>
struct HuffmanTreeNode //定义哈弗曼树节点的类型
{
HuffmanTreeNode(const W& weight)
:_pLeft(nullptr)
, _pRight(nullptr)
, _pParent(nullptr)
, _weight(weight)
{
}
HuffmanTreeNode<W>* _pLeft;
HuffmanTreeNode<W>* _pRight;
//用孩子双亲形式表示二叉树,方便从叶子结点找到根节点,从而对字符进行编码
HuffmanTreeNode<W>* _pParent;
W _weight; //节点权值
};
template<class W>
struct Compare //用仿函数比较,变小堆
{
typedef HuffmanTreeNode<W>* PNode;
bool operator()(const PNode pLeft, const PNode pRight)
{
return pLeft->_weight > pRight->_weight;
}
};
template<class W>
class HuffmanTree
{
typedef HuffmanTreeNode<W> Node;
typedef Node* PNode;
public:
HuffmanTree()//构造函数,初始状态下为空树
: _pRoot(nullptr)
{
}
void CreatHuffmanTree(const std::vector<W>& v, const W& invalid) //根据权值创建树,模板引用 vector在标准的命名空间中定义的
{
if (v.empty()) //v是存放权值的数组
return;
//用所给的权值创建二叉树森林
//std::priority_queue<PNode> q; 使用仿函数实现 小堆 比较器
std::priority_queue<PNode,std::vector<PNode>,Compare<W>> q;//优先级队列 保存树,把地址放进去就好了,,但是默认是大堆,而我们需要的是小堆
for (size_t i = 0; i < v.size(); ++i)
{
if (v[i] != invalid) //过滤出现0次的字符,通过创建哈弗曼树多加上一个参数 来实现
q.push(new Node(v[i])); //用权值创建节点
}
while (q.size() > 1) //当树不止一个时,把作为左右孩子
{
PNode pLeft = q.top();
q.pop();
PNode pRight = q.top