文章目录
1.上机名称
构造哈夫曼树和生成哈夫曼编码
2.上机要求
(1) 输入一段文本,统计文本中每个字符出现的频率
(2) 对每个字符构造哈夫曼树,输出对应的哈夫曼编码
(3) 输出文本对应的编码
(4) 对编码进行译码,输出译码后的文本,并判断是否和输入文本一致
注:对实验内容进行了扩充,要求:
(5) 从文本读取数据,输出字符数量和对应编码,将英文字符文件编码输出到另一个文件
(6) 将编码出来的文件译码回原文件
3.上机环境
visual studio 2022 Windows11 家庭版 64位操作系统
4.程序清单(写明运行结果及结果分析)
4.1 程序清单
4.1.1 head.h 头文件内容如下:
#pragma once
#include<iostream>
#include<string>
using namespace std;
#define Buff_Size 1024 //定义从文件一次读取的字符串大小
#define Buff_size 64
#define Tree_Size 64
#define InputFile_Name "test1.txt" //输入的原始文件名,可更改
#define OutputCode_Name "OutCode.txt" //输出编码的文件名,可更改
#define OutputFile_Name "OutText.txt" //通过编码文件输出的文件名,可更改
typedef struct Tnode { //哈夫曼树的节点
int count; //用次数代替频率
char data; //存放的字符内容
struct Tnode* parent; //双亲
struct Tnode* lchild; //左子树
struct Tnode* rchild; //右子树
string code; //保存的编码
}Node, * pNode;
typedef struct CodeRecorder { //编码表
char data;
string code;
}Cd, * pCd;
class hfmtree {
public:
hfmtree(); //构造对象
~hfmtree(); //析构函数
void Analyse(); //统计字符
void maketree(); //生成哈夫曼树
void encode(); //内部生成编码
pCd outcode(); //导出编码表
void outputcode(pCd cd); //通过给定的编码表,输出编码后的文件
void outputfile(pCd cd); //将编码文件转为字符文件输出
private:
//所有树的节点,原本只需要26个英文字母和6个符号,
//包括 , . : ? 引号(作为一类了)空格的存储空间,
//但需要预留空节点去作为连接节点
//粗略估计需要64个节点
Node treeinfo[Tree_Size];
//显示目前已用的空间,便于资源调度:
int usedlen;
//经过处理得到的哈夫曼树的根
Node root;
//交换数据的缓冲区,一大一小:
char Buff[Buff_Size];
char buff[Buff_size];
};
4.1.2 head.cpp 实现文件内容如下:
#include "head.h"
hfmtree::hfmtree(){//创建
for (int i = 0; i < Tree_Size; i++) {
treeinfo[i].count = 0;
treeinfo[i].data = '#';
treeinfo[i].lchild = nullptr;
treeinfo[i].rchild = nullptr;
treeinfo[i].parent = nullptr;
treeinfo[i].code = ""; //空串
}
for (int i = 0; i < 26; i++) {
treeinfo[i].data = i + 'a';
}
treeinfo[26].data = ' '; //空格
treeinfo[27].data = ','; //逗号
treeinfo[28].data = '.'; //句号
treeinfo[29].data = '?'; //问号
treeinfo[30].data = '!'; //感叹
treeinfo[31].data = '\"'; //引号
usedlen = 32;
}
hfmtree::~hfmtree(){
}
void hfmtree::Analyse(){
FILE* fp;
fopen_s(&fp, InputFile_Name, "rb");
fseek(fp, 0, SEEK_SET);
int i = 0;
while (!feof(fp)) {
memset(Buff, 0, Buff_Size);
if (fread(&Buff[0], 1, Buff_Size, fp) == 0) exit(10);
while (i < Buff_Size)
{
if ('a' <= Buff[i] && Buff[i] <= 'z') {
++treeinfo[Buff[i] - 'a'].count;
}
else if ('A' <= Buff[i] && Buff[i] <= 'Z') {
++treeinfo[Buff[i] - 'A'].count;
}
else {
switch (Buff[i]){
case ' ': ++treeinfo[26].count; break;
case ',': ++treeinfo[27].count; break;
case '.': ++treeinfo[28].count; break;
case '?': ++treeinfo[29].count; break;
case '!': ++treeinfo[30].count; break;
case '\'':
case '\"': ++treeinfo[31].count; break;
default: break;
}
}
i++;
}
i = 0;
}
fclose(fp);
for (int i = 0; i < usedlen;i++) {
cout <<treeinfo[i].data << ":" << treeinfo[i].count<<" ";
}
//system("pause");
}
void hfmtree::maketree(){
int cnt_frst_num;
while (1) {
//计算森林里树的个数,如果只剩一棵树,说明构造完成,退出循环:
cnt_frst_num = 0;//代表树的数量 count forest's number
for (int i = 0; i < usedlen; i++) {
if (treeinfo[i].parent == nullptr) { //没有双亲的就是树根
cnt_frst_num++; //有一个树根就有一棵树
}
}
if (cnt_frst_num == 1) break; //如果只有一颗树退出while
//在有多棵树的情况下:
//准备一个新节点:
pNode nodenew = &treeinfo[usedlen];
//找到第一个有效节点,将它作为基准值:
int first_id = 0;
for (int i = 0; i < Tree_Size; i++) {
if (treeinfo[i].parent == nullptr && treeinfo[i].data != '#') {
first_id = i;
break;
}
}
for (int i = 0; i < Tree_Size; i++) {
if (treeinfo[i].parent == nullptr && treeinfo[i].data != '#') {
first_id = treeinfo[first_id].count <= treeinfo[i].count ? first_id : i;
}
}
pNode node1 = &treeinfo[first_id];//拿到count最小的有效节点
//下面拿到count第二小的有效节点
int second_id = 0;
for (int i = 0; i < Tree_Size; i++) {
if (treeinfo[i].parent == nullptr && treeinfo[i].data != '#' && i != first_id) {
second_id = i;
break;
}
}
for (int i = 0; i < Tree_Size; i++) {
if (treeinfo[i].parent == nullptr && treeinfo[i].data != '#' && i != first_id) {
second_id = treeinfo[second_id].count <= treeinfo[i].count ? second_id : i;
}
}
pNode node2 = &treeinfo[second_id];//拿到count第二小的有效节点
//下面将两个节点连接到双亲节点上
node1->parent = nodenew;
node2->parent = nodenew;
nodenew->lchild = node1;
nodenew->rchild = node2;
//使nodenew在下次循环中有效
nodenew->data = '^';
nodenew->count = node1->count + node2->count;
usedlen++;
}
cout <<"Total char:"<< treeinfo[usedlen - 1].count << endl; //在以上操作中,treeinfo[usedlen - 1]为哈夫曼树树根
}
void hfmtree::encode() {
for (int i = 0; i <= usedlen; i++) {
for (int j = 0; j <= usedlen; j++) {
if (treeinfo[j].lchild != nullptr)treeinfo[j].lchild->code = treeinfo[j].code + "0";
if (treeinfo[j].rchild != nullptr)treeinfo[j].rchild->code = treeinfo[j].code + "1";
}
}
}
pCd hfmtree::outcode(){
pCd code = new Cd[32];
for (int i = 0; i < 32; i++) {
code[i].data = treeinfo[i].data;
code[i].code = treeinfo[i].code;
}
return code;
}
void hfmtree::outputcode(pCd cd){
string Buffstring;
FILE* ifp,*ofp;
fopen_s(&ifp, InputFile_Name, "rb");
fopen_s(&ofp, OutputCode_Name, "w");
while (!feof(ifp)) {
memset(buff, 0, Buff_size);
Buffstring.clear();
fread(buff, 1, Buff_size, ifp);
for (auto v : buff) {
if ('a' <= v && v <= 'z') {
Buffstring += cd[v - 'a'].code;
}
else if ('A' <= v && v <= 'Z') {
Buffstring += cd[v - 'A'].code;
}
else switch (v) {
case ' ': Buffstring += cd[26].code; break;
case ',': Buffstring += cd[27].code; break;
case '.': Buffstring += cd[28].code; break;
case '?': Buffstring += cd[29].code; break;
case '!': Buffstring += cd[30].code; break;
case '\'':
case '\"': Buffstring += cd[31].code; break;
default: break;
}
}
//cout << Buffstring << endl;
fwrite(Buffstring.c_str(), 1, Buffstring.length(), ofp);
//fputc('\n', ofp);
}
fclose(ifp);
fclose(ofp);
}
void hfmtree::outputfile(pCd cd){
string Buffstring = "";
string cmpstring = "";
FILE* ifp, * ofp;
fopen_s(&ifp, OutputCode_Name, "r");
fopen_s(&ofp, OutputFile_Name, "w");
while (!feof(ifp)) {
Buffstring.clear();
memset(Buff, 0, Buff_Size);
fread(Buff, 1, Buff_Size, ifp);
//cout << "read:" << Buff << endl;
for (int i = 0; i < Buff_Size; i++) {
cmpstring += Buff[i];
for (int j = 0; j < 32;j++) {
if (cmpstring.compare(cd[j].code) == 0) {
//cout << "compare:" << cmpstring << endl;
Buffstring += cd[j].data;
cmpstring.clear();
}
}
}
//cout << "write:"<< Buffstring << endl;
fwrite(Buffstring.c_str(),Buffstring.length(), 1 , ofp);
}
fclose(ifp);
fclose(ofp);
}
4.1.3 源文件main.cpp 内容如下:
#include"head.h"
int main() {
hfmtree tree;
tree.Analyse();
tree.maketree();
tree.encode();
pCd cd = tree.outcode();
//print code_recorder:
for (int i = 0; i < 32; i++) {
cout << cd[i].data << ':' << cd[i].code << endl;
}
tree.outputcode(cd);
tree.outputfile(cd);
}
4.2 程序运行结果
注:在程序目录下放了一本英文小说test1.txt,作为数据的输入,正文开头内容如下:
选中所有文本,发现有28万左右的字:
启动程序运行,命令行提示字符统计数(仅统计设计好的字符)以及对应编码信息,发现空格和元音字母在英语文本中出现的频率最高,在哈夫曼编码中对应字符编码也最短,j q z出现的频率最低,在哈夫曼编码中对应字符编码也最长,符合实验正确现象:
打开目录,发现在程序目录下生成了另外两个文件,即用编码输出的文件和译码回去的文件,点击打开,进行对照,发现编码能够唯一确定字符,而文本由于没有设置处理格式、数字和特殊字符,导致只能将纯英文内容进行输出,部分不支持处理的字符丢失导致字数减少,只剩27万左右:
5.上机体会
哈夫曼树和哈夫曼编码是数据结构中非常重要的概念和算法,通过实验来加深对它们的理解是非常有意义的。以下是我在进行构造哈夫曼树和生成哈夫曼编码实验后得到的一些心得:
- 理解哈夫曼树和哈夫曼编码的原理和应用场景非常重要。哈夫曼树是一种最优二叉树,可以用于压缩数据,减少存储空间。哈夫曼编码是基于哈夫曼树的一种编码方式,可以用于数据的传输和存储,减少带宽和存储空间的占用,在本次实验中,较之Unicode编码,压缩率就可以达到25.7%。
- 在实验中,需要掌握哈夫曼树的构造算法和哈夫曼编码的生成算法。哈夫曼树的构造算法包括选择最小权值的两个节点、合并节点、更新权值等步骤,需要注意每一步的细节和实现方式。哈夫曼编码的生成算法需要根据哈夫曼树的结构,对每个节点进行编码,得到一个二进制编码字符串,本次实验中没有用到容器进行自下而上的栈式实现编码,而是通过空间换时间,利用函数递归完成对每个节点的编码。
- 通过实验,可以深入理解哈夫曼树和哈夫曼编码的性能和优缺点。哈夫曼树和哈夫曼编码可以有效地减少数据的存储空间和传输带宽,但也存在一些局限性,例如对于非等概率分布的数据,压缩效果可能不佳。因此,在实际应用中需要根据具体情况选择合适的压缩算法和编码方式。
- 最后,通过实验还可以提高自己的编程能力和数据结构的应用能力。实验需要编写复杂的算法和代码,需要运用数据结构的知识和技能,同时也需要具备良好的编程习惯和调试能力。通过实验,可以不断提高自己的编程水平和解决问题的能力。