一、实验目标
- 掌握树的存储结构。
- 掌握二叉树的三种遍历方法。
- 掌握 Huffman 树、Huffman 编码等知识和应用。
- 使用 C++、文件操作和 Huffman 算法实现 “图片压缩程序” 专题编程。
二、实验任务
压缩软件是利用特定算法来压缩数据的工具,压缩后生成的文件称为压缩包(archive)。如果想使用其中的数据,就得用压缩软件对数据进行解压。利用压缩软件对文件中重复的数据进行压缩,可以减小文件中的字节总数,使文件能够通过互联网连接实现更快传输,此外还可以减少文件的磁盘占用空间。常用的压缩格式有 rar、zip 等。
压缩可以分为无损压缩与有损压缩两种。无损压缩后的文件,经过解压能够完全恢复原始数据;有损压缩的文件则无法完全恢复。rar、zip 等格式都是无损压缩格式。音乐文件格式 mp3、图片文件格式 jpg 都是有损压缩格式。
计算机文件是由一个个字节组成的,1 个字节有 0~255 共 256 种可能的值,每个字节的编码长度都是 8 位。由于文件中的字节总是会重复出现,可以对不同的字节设计长度不等的编码,让出现次数较多的字节,采用尽可能短的编码,那么文件编码的总长便可减少。
统计文件中 256 种不同的字节重复的次数,以每种字节重复的次数作为权值(weight)构造一棵有 256 个叶子节点的二叉树。若带权路径长度达到最小,称这样的二叉树为最优二叉树,即 Huffman 树(Huffman tree)。
Huffman 树从根到每个叶子都有一条路径。对路径上的各分支,约定指向左子树根的分支编码为 “0”,指向右子树根的分支编码为 “1”。从根到每个叶子相应路径上的 “0” 和 “1” 组成的序列,就是这个叶子节点的编码,称为 Huffman 编码。
功能要求:使用 Huffman 压缩算法,对一幅 BMP 格式的图片文件进行压缩。图片文件名为 “Pic.bmp”,内容如下图所示。压缩后保存为 “Pic.bmp.huf” 文件。
使用 Visual Studio 2019 作为开发工具,开发一个控制台程序,使用 Huffman 压缩算法对图片文件 “Pic.bmp” 进行压缩,具体要求如下:
- 创建工程:创建 HuffmanCompress 工程,定义入口函数 int main()。
- 读取原文件:读取文件,统计 256 中字节重复的次数。
- 生成 Huffman 树:根据上一步的统计结果,构建 Huffman 树。
- 生成 Huffman 编码:遍历 Huffman 树,记录 256 个叶子节点的路径,生成 Huffman 编码。
- 压缩编码:使用 Huffman 编码,对原文件中的字节重新编码,获得压缩后的文件数据。
- 保存文件:将编码过的数据,保存到文件 “Pic.bmp.huf” 中。
三、分析和设计
通过需求可知,本程序首先需要通过读取文件,生成一棵带权二叉树。树在程序中可使用链式结构、顺序结构两种方式来表示。已知这棵树的叶子节点数为 256,其存储空间是固定的,因此,本程序将使用顺序结构来表示这棵二叉树。
5.3.1 编码构思
- 定义存储结构:定义一个结构体 HTNode 来表示二叉树的叶子节点,记录每个节点的权值、父节点、左孩子和右孩子。创建结构体数组 HTNode aHuffmanTree[512] 来存储这棵带权二叉树。
- 遍历算法:其次,生成 Huffman 树后,要遍历这棵二叉树,根据叶子节点的路径来生成 Huffman 编码表。遍历二叉树有先序遍历、中序遍历、后序遍历三种方式。本程序将使用先序遍历的方式,查找二叉树上所有的叶子节点,生成对应的 Huffman 编码。定义二维字符数组 char[256][] 来存储所有叶子节点的 Huffman 编码,供编码时使用。
- 压缩编码:由于 Huffman 编码表是以字符数组的形式保存的,重新编码后的数据将是一个很长的字符串。定义 Str2byte 函数,将形如 “01010101” 字符串转换成字节,才能得到最终的编码。将其保存到 “*.huf” 中,即实现了文件压缩。
- 解压缩:最后,为了保证压缩后的数据能够被正确解压,除了保存压缩的数据之外,还应保存原文件的长度及 256 种字节的重复次数。定义一个文件头,用于保存这些信息。在保存压缩文件时,同时向文件中写入文件头和压缩数据,以保证文件能够被还原。
5.3.2 程序设计
- 创建 Main.cpp 文件,定义 int main(void) 函数,作为程序的入口函数。
- 创建 Huffman.h 文件与 Huffman.cpp 文件,实现 Huffman 算法相关的函数。
- 定义 HTNode 结构体。
- 定义 HuffmanTree 函数,用于生成 Huffman 树。
- 定义 HuffmanCode 函数,用于生成 Huffman 编码。
- 创建 Compress.h 文件与 Compress.cpp 文件,实现文件的压缩算法。Compress.cpp 文件中将包含 Huffman.h 文件,使用其中的 Huffman 算法相关的函数。
- 定义 Compress 函数,用于实现文件压缩。
- 定义 InitHead 函数,读取原文件,初始化文件头数据信息。
- 定义 Encode 函数,利用 Huffman 编码实现文件压缩。
- 定义 Str2byte 函数,用于将“01010101”形式的字符串字节。
- 定义 WriteFile 函数,用于将压缩后的数据写入新文件。
5.3.3. 业务流程
使用 Huffman 算法压缩图片,业务流程如下:
四、运行示例
五、程序源码
5.1 main.cpp
#include <iostream>
#include "Compress.h"
using namespace std;
int main(void)
{
cout << "========== Huffman 文件压缩 ==========" << endl;
cout << "请输入文件名:";
char filename[256];
cin >> filename;
if (Compress(filename) == 1)
cout << "\n文件压缩成功!" << endl;
else
cout << "\n文件压缩失败!" << endl;
return 0;
}
5.2 global.h
#pragma once
#define OK 1
#define ERROR 0
5.3 Compress.h
#pragma once
// Encode 函数声明会用到 HuffmanCode
#include "Huffman.h"
// 文件头
struct HEAD
{
char type[4];
int length;
int weight[256];
};
// 实现文件压缩
int Compress(const char *pFilename);
// 读取源文件和初始化头文件的信息
int InitHead(const char *pFilname, HEAD &sHead);
// 利用 Huffman 编码实现压缩编码
int Encode(const char *pFilname, const HuffmanCode pHC, char *pBuffer, const int nSize);
// 将二进制字符串转换成字节
char Str2byte(const char *pBinStr);
// 生成压缩文件
int WriteFile(const char *pFilename, const HEAD sHead, const char *pBuffer, const int nSize);
5.4 Compress.cpp
#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
#include <stdlib.h>
#include <string.h>
#include "Compress.h"
#include "Huffman.h"
#include "global.h"
using namespace std;
const int SIZE = 256;
// 实现文件压缩
int Compress(const char *pFilename)
{
cout << endl;
// 打开并扫描文件
cout << "正在读取文件……" << endl
<< endl;
int weight[256] = {0};
FILE *in = fopen(pFilename, "rb");
int tempch;
// 获取权重
while ((tempch = getc(in)) != EOF)
weight[tempch]++;
int temp;
cout << "输入数字“1”显示256种字节出现次数,输入其它数字则不显示!" << endl;
cout << "请输入您的选择:";
cin >> temp;
cout << endl;
if (temp == 1)
{
// 测试,显示256种字节出现的次数
showWeight(weight);
cout << endl;
}
cout << "文件读取完毕!\n"
<< endl;
// 关闭文件
fclose(in);
// 将编码生成Huffman树
// Huffman树共有n个叶子节点
int n = 256;
// 那么就有2n+1个节点
int m = 2 * n - 1;
// 定义Huffman树
HuffmanTree pHT = new HTNode[m + 1];
CreateHuffmanTree(pHT, weight, n);
int temp1;
cout << "输入数字“1”显示Huffman 树每个节点的信息,输入其它数字则不显示!" << endl;
cout << "请输入您的选择:";
cin >> temp1;
cout << endl;
if (temp1 == 1)
{
// 测试,输出 Huffman 树每个节点的信息
TestHufTree(pHT);
cout << endl;
}
// 生成Huffman编码
char **pHC = new char *[n + 1]; // 编码
for (int i = 1; i <= n; i++)
pHT[i].weight = weight[i - 1];
HuffmanCoding(pHC, pHT);
int temp2;
cout << "输入数字“1”显示字节的Huffman编码信息,输入其它数字则不显示!" << endl;
cout << "请输入您的选择:";
cin >> temp2;
cout << endl;
if (temp2 == 1)
{
// 测试,显示字节的Huffman编码信息
cout << "\n哈夫曼树的编码信息为:" << endl;
cout << "Byte\tHuffmanCode" << endl;
TestHufCode(511, pHT, pHC);
cout << endl;
}
// 计算编码缓冲区大小
int nSize = 0;
for (int i = 0; i < 256; i++)
nSize += weight[i] * strlen(pHC[i + 1]);
nSize = (nSize % 8) ? nSize / 8 + 1 : nSize / 8;
// 对编码文件进行压缩
char *pBuffer = NULL;
pBuffer = new char[nSize];
memset(pBuffer, 0, (nSize) * sizeof(char));
Encode(pFilename, pHC, pBuffer, nSize);
if (!pBuffer)
{
return ERROR;
}
HEAD sHead;
InitHead(pFilename, sHead);
cout << "原文件大小:" << sHead.length << "字节" << endl;
int afterlen = WriteFile(pFilename, sHead, pBuffer, nSize);
cout << "压缩后文件大小:" << afterlen << "字节" << endl;
cout << "压缩比率:" << (double)afterlen * 100 / sHead.length << "%" << endl;
delete pHT;
delete[] pHC;
delete pBuffer;
return OK;
}
// 扫描文件和初始化头文件的信息
int InitHead(const char *pFilname, HEAD &sHead)
{
// 文件类型
strcpy(sHead.type, "HUF");
// 源文件长度
sHead.length = 0;
for (int i = 0; i < SIZE; i++)
// 权值
sHead.weight[i] = 0;
// 以二进制流形式打开文件
FILE *in = fopen(pFilname, "rb");
// 扫描文件,获得权重
int ch;
while ((ch = fgetc(in)) != EOF)
{
sHead.weight[ch]++;
sHead.length++;
}
// 关闭文件
fclose(in);
in = NULL;
return OK;
}
// 实现压缩编码
int Encode(const char *pFilname, const HuffmanCode pHC, char *pBuffer, const int nSize)
{
// 打开文件
FILE *in = fopen(pFilname, "rb");
// 开辟缓冲区ni
pBuffer = (char *)malloc(nSize * sizeof(char));
if (!pBuffer)
cout << "开辟缓冲区失败!" << endl;
// 工作区
char cd[SIZE] = {0};
// 缓冲区指针
int pos = 0;
int ch;
// 扫描文件
while ((ch = fgetc(in)) != EOF)
{
strcat(cd, pHC[ch + 1]);
// 压缩编码
while (strlen(cd) >= 8)
{
pBuffer[pos++] = Str2byte(cd);
for (int i = 0; i < SIZE - 8; i++)
{
cd[i] = cd[i + 8];
}
}
}
if (strlen(cd) > 0)
{
pBuffer[pos++] = Str2byte(cd);
}
fclose(in);
return OK;
}
// 生成压缩文件
int WriteFile(const char *pFilename, const HEAD sHead, const char *pBuffer, const int nSize)
{
// 生成文件名
char filename[256] = {0};
strcpy(filename, pFilename);
strcat(filename, ".huf");
// 以二进制流形式打开文件
FILE *out = fopen(filename, "wb");
// 写文件
fwrite(&sHead, sizeof(HEAD), 1, out);
// 写压缩后的编码
fwrite(pBuffer, sizeof(char), nSize, out);
// 关闭文件,释放文件指针
fclose(out);
out = NULL;
cout << "生成压缩文件:" << filename << endl;
int len = sizeof(HEAD) + strlen(pFilename) + 1 + nSize;
return len;
}
// 将字符串转换成字节
char Str2byte(const char *pBinStr)
{
char b = 0x00;
for (int i = 0; i < 8; i++)
{
// 左移一位
b = b << 1;
if (pBinStr[i] == '1')
{
b = b | 0x01;
}
}
return b;
}
5.5 Huffman.h
#pragma once
// Huffman 树节点
typedef struct
{
int weight; // 权值
int parent; // 父节点
int lchild; // 左孩子
int rchild; // 右孩子
} HTNode, *HuffmanTree;
// Huffman编码
typedef char **HuffmanCode;
// 显示 256 种字节的出现的次数
void showWeight(int weight[]);
// 生成Huffman树
int CreateHuffmanTree(HuffmanTree pHT, int weight[], int n);
// 生成Huffman编码
int HuffmanCoding(HuffmanCode &pHC, HuffmanTree &pHT);
// 查找 Huffman 树结点数组中权值最小的节点
void Select(HuffmanTree &HT, int i, int &s1, int &s2);
// 测试函数,输出哈夫曼树的每个节点信息
int TestHufTree(HuffmanTree pHT);
// 测试函数,采用先序遍历的方法,输出 Huffman 树每个节点的信息
void TestHufCode(int root, HuffmanTree &pHT, HuffmanCode &pHC);
5.6 Huffman.cpp
#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
#include <malloc.h>
#include <string.h>
#include "Huffman.h"
#include "global.h"
using namespace std;
// 显示 256 种字节的出现的次数
void showWeight(int weight[])
{
cout << "原文件每个字符的权值为:" << endl;
cout << "Byte\t"
<< "Weight\t" << endl;
for (int i = 0; i < 256; i++)
printf("0x%02X\t%d\n", i, weight[i]);
}
// 生成Huffman树
int CreateHuffmanTree(HuffmanTree pHT, int weight[], int n)
{
int s1, s2, i;
int m = 2 * n - 1;
// 初始化
for (i = 1; i <= n; i++)
{
pHT[i].weight = weight[i - 1];
pHT[i].lchild = 0;
pHT[i].rchild = 0;
pHT[i].parent = 0;
}
for (i = n + 1; i <= m; i++)
{
pHT[i].parent = 0;
pHT[i].lchild = 0;
pHT[i].rchild = 0;
pHT[i].weight = 0;
}
for (i = n + 1; i <= m; i++)
{
// 从pHT[1...i-1]中选择parent为0且weight最小的两个结点,其序号分别为s1和s2
Select(pHT, i - 1, s1, s2);
pHT[s1].parent = i;
pHT[s2].parent = i; // 修改s1和s2结点的父指针parent
pHT[i].lchild = s1;
pHT[i].rchild = s2; // 修改i结点的左右孩子指针
pHT[i].weight = pHT[s1].weight + pHT[s2].weight; // 修改权值
}
return OK;
}
// 查找Huffman树节点数组中权值最小的节点
void Select(HuffmanTree &pHT, int i, int &s1, int &s2)
{
int minValue = 0x7FFFFFFF;
// 找到最小的一个权值
for (int j = 1; j <= i; j++)
{
if (pHT[j].parent == 0 && pHT[j].weight < minValue)
{
minValue = pHT[j].weight;
s1 = j;
}
}
minValue = 0x7FFFFFFF;
// 找到倒数第二小的权值
for (int j = 1; j <= i; j++)
{
if (j != s1 && pHT[j].parent == 0 && pHT[j].weight < minValue)
{
minValue = pHT[j].weight;
s2 = j;
}
}
}
// 生成Huffman编码
int HuffmanCoding(HuffmanCode &pHC, HuffmanTree &pHT)
{
// 无栈非递归遍历Huffman树,求Huffman编码
// 记录访问路径
char cd[256] = {'\0'};
// 记录当前路径长度
int cdlen = 0;
// 遍历Huffman树时用做节点的状态标志
for (int i = 1; i < 512; i++)
pHT[i].weight = 0;
int p = 511;
while (p != 0)
{
// 向左
if (pHT[p].weight == 0)
{
pHT[p].weight = 1;
if (pHT[p].lchild != 0)
{
p = pHT[p].lchild;
cd[cdlen++] = '0';
}
// 登记叶子节点的字符的编码
else if (pHT[p].rchild == 0)
{
pHC[p] = (char *)malloc((cdlen + 1) * sizeof(char));
cd[cdlen] = '\0';
strcpy(pHC[p], cd); // 复制编码
}
}
// 向右
else if (pHT[p].weight == 1)
{
pHT[p].weight = 2;
// 右孩子为叶子节点
if (pHT[p].rchild != 0)
{
p = pHT[p].rchild;
cd[cdlen++] = '1';
}
}
// 退回父节点,编码长度减一
else
{
pHT[p].weight = 0;
p = pHT[p].parent;
cdlen--;
}
}
return OK;
}
// 测试函数,输出哈夫曼树的每个节点信息
int TestHufTree(HuffmanTree pHT)
{
cout << "哈夫曼树的每个节点信息为:" << endl;
cout << "Byte\t\tWeight\tParent\tLchild\tRchild\n";
for (int i = 1; i < 512; i++)
{
// 判断语句为了对齐格式
if (i <= 99)
cout << "pHT[" << i << "]\t\t" << pHT[i].weight << "\t" << pHT[i].parent << "\t" << pHT[i].lchild << "\t" << pHT[i].rchild << endl;
else
cout << "pHT[" << i << "]\t" << pHT[i].weight << "\t" << pHT[i].parent << "\t" << pHT[i].lchild << "\t" << pHT[i].rchild << endl;
}
return OK;
}
// 测试函数,采用先序遍历的方法,输出 Huffman 树每个节点的信息
void TestHufCode(int root, HuffmanTree &pHT, HuffmanCode &pHC)
{
if (root <= 1)
return;
if (pHT[root].lchild == 0 && pHT[root].rchild == 0)
printf("0x%02X\t%s\n", root - 1, pHC[root - 1]);
if (pHT[root].lchild) // 访问左孩子
TestHufCode(pHT[root].lchild, pHT, pHC);
if (pHT[root].rchild) // 访问右孩子
TestHufCode(pHT[root].rchild, pHT, pHC);
}