无偿分享以前做的一些实验报告,喜欢点赞+关注(*^_^*)
源代码也附带了,大家有需要可以自己复制走~
时间久远,代码很稚嫩,适合大一大二的宝宝\( ̄︶ ̄*\)),有问题可以留言,有缘会回复
- 实验题目:
用哈夫曼编码实现文件压缩
二、实验目的:
- 了解文件的概念。
- 掌握线性链表的插入、删除等算法。
3、掌握Huffman树的概念及构造方法。
4、掌握二叉树的存储结构及遍历算法。
5、利用Huffman树及Huffman编码,掌握实现文件压缩的一般原理。
三、实验设备与环境:
微型计算机、Windows 7操作系统 、Visual C++6.0软件
四、实验内容:
压缩过程是根据Ascii码文件以字节为单位,统计0~255中每个数字出现的次数,依此构建哈夫曼树,获得编码表,然后按照编码表将文件重新编码。其中,将哈夫曼树使用01序列表示,给解压提供其所需的哈夫曼树的结构。存储在压缩后的文件头部,以便解压时使用。解压过程是读取文件头部的信息,构建哈夫曼树,获得编码表,反向解析成原文件。最后对比发现解压之后的文件与原文件相同,达成学习掌握赫夫曼树等的实验目的。
五、系统设计:
内容
①执行样例如下:
主函数->可选择相应功能(0-2):
1.压缩
2.解压缩
0.关闭系统
选择1->输入待压缩文件a.txt->输入想要的压缩后文件aa.zip
选择2->输入待解压文件aa.zip->输入想要的解压后文件aaa.txt
*该程序会列出出现字符的01序列并计算压缩率。
*对比发现,两者文本相同。
选择3->感谢使用->关闭
②压缩、解压大致流程:
Ⅰ.压缩文件
一、打开文件
二、读取文件至缓冲区
三、读取这片缓冲区,将其压缩:
1.以字节为单位统计0~255出现的频率
⒉.根据频率构建哈夫曼树
3.将哈夫曼树编码成01序列:
4.获取编码表
5.计算压缩后文件的长度、结尾将有多少位无效位
6.将数据依次写入压缩结果缓冲区,包括:无效位长度,占3比特
- 将压缩结果写入文件
- 关闭文件
Ⅱ.解压文件:
一、打开文件
二、读取文件至缓冲区
三、读取这片缓冲区,将其解压:
1.解析提取结尾无效bit的长度
2.获取表示树结构的01序列
3.获取表示叶子信息的字节序列
4.构建哈夫曼树
5.根据哈夫曼树解压数据
四、将解压结果写入文件
五、关闭文件
六、系统实现及测试结果:
源代码:
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <conio.h>
#include<iostream>
//#include<vector>
using namespace std;
struct head
{
unsigned char b; //记录字符在数组中的位置 //四个字节
long count; //字符出现频率(权值)
long parent, lch, rch; //定义哈夫曼树指针变量
char bits[256]; //定义存储哈夫曼编码的数组
}jd[512], tmp;//结点数组,其中叶子结点在下标小的,所有的叶子结点在前面(0~255),非叶子结点在后面(256~511)。
/*压缩*/
void ys()
{
char name[255], outname[255], buf[512];
unsigned char c;
long i, j, m, n, f;
long min1, pt1, flength, length1, length2;
double div;
FILE *ifp, *ofp;
printf("\t请您输入需要压缩的文件(包括拓展名):");
gets(name);
ifp = fopen(name, "rb");
/*fopen函数返回新打开文件的文件指针;
如果此文件不能打开,则返回NULL指针。
"rb" 为读而打开二进制文件
"wb" 为写而打开二进制文件 */
if (ifp == NULL)
{
printf("\n\t文件打开失败!\n\n");
return;
}
printf("\t请您输入压缩后的文件(包括拓展名):");
gets(outname);
ofp = fopen(outname, "wb");
if (ofp == NULL)
{
printf("\n\t压缩文件失败!\n\n");
return;
}
flength = 0;
while (!feof(ifp))//一直到文件末尾
{
fread(&c, 1, 1, ifp);
/*fread函数用于从文件流中读取数据,其函数原型为:
size_t fread(void* buffer, size_t size, size_t count, FILE*stream);
fread函数每次从stream中最多读取count个单元,每个单元大小为size个字节,将读取的数据放到buffer;文件流的指针后移size*count字节。*/
jd[c].count++; //字符重复出现频率+1
flength++; //字符出现原文件长度+1
}
flength--;
length1 = flength; //原文件长度用作求压缩率的分母
jd[c].count--;
for (i = 0; i < 512; i++)
{
if (jd[i].count != 0) jd[i].b = (unsigned char)i;
/*将每个哈夫曼码值及其对应的ASCII码存放在一维数组jd[i]中,
且编码表中的下标和ASCII码满足顺序存放关系*
通过循环,对权值不为0的结点通过强制转型赋与其下标一致的字符值,权值为0的不计。*/
else jd[i].b = 0;
jd[i].parent = -1;
jd[i].lch = jd[i].rch = -1;
/*对父节点和孩子赋初始值。
图片中可能会出现256种字节,但可能某些字节并未出现,
因此判断HuffTreeNode数组前256个元素有权值的元素,
让这些元素做为Huffman树的叶子结点,其它的元素不参于树的生成,提高效率。*/
}
for (i = 0; i < 256; i++) //根据频率(权值)大小,对结点进行排序,选择较小的结点进树
{
for (j = i + 1; j < 256; j++)
{
if (jd[i].count < jd[j].count)
{
tmp = jd[i];
jd[i] = jd[j];
jd[j] = tmp;
}
}//冒泡排序,两两比较,时间复杂度是on2
}
for (i = 0; i < 256; i++) if (jd[i].count == 0) break;//记录叶子节点的个数,得到哈夫曼树的总结点数。
n = i; //外部叶子结点数为n个时,内部结点数为n-1,整个哈夫曼树的需要的结点数为2*n-1.
m = 2 * n - 1;
for (i = n; i < m; i++) //构建哈夫曼树
{
min1 = 0x3f3f3f3f; //预设的最大权值,即结点出现的最大次数
for (j = 0; j < i; j++)
{
if (jd[j].parent != -1) continue;
//parent!=-1说明该结点已存在哈夫曼树中,跳出循环重新选择新结点*/
if (min1 > jd[j].count)
{
pt1 = j;
min1 = jd[j].count;
continue;
}
}
jd[i].count = jd[pt1].count;
jd[pt1].parent = i; //依据parent域值(结点层数)确定树中结点之间的关系
jd[i].lch = pt1; //计算左分支权值大小
min1 = 0x3f3f3f3f;
for (j = 0; j < i; j++)
{
if (jd[j].parent != -1) continue;
if (min1 > jd[j].count)
{
pt1 = j;
min1 = jd[j].count;
continue;
}
}
jd[i].count += jd[pt1].count;
jd[i].rch = pt1; //计算右分支权值大小
jd[pt1].parent = i;
}
/*每次选HuffTreeNode数组中权值最小的两个元素,其中最小值作为树的左孩子,次小值做为树的右孩子,
构建哈夫曼树的后n-1个结点,先预设一个最大权值,通过循环,parent!=-1 说明该结点已在哈夫曼树中,
跳出循环重新选择新节点,通过比较找到最小结点和次小结点。然后将该树的根结点作为Huffuman树的非叶子结点。
为了保存它们之间的逻辑关系,保存左右孩子的父节点设置,将根节点的权值设置为左右孩子权值之和,
将根节点的左右孩子设置。在parent值是0的结点中选取count值最小的两个结点进行合并,
规定最小的合并为左孩子,第二小值合并为右孩子,合并生成的根结点的count值为左右孩子权值之和。
重复上述过程,直到所有结点合并成一棵树。(parent=0,表示该结点是整棵树的根结点,lch=0和rch=0则表示该结点是叶子结点。)
*/
for (i = 0; i < n; i++) //哈夫曼无重复前缀编码
{
f = i;
jd[i].bits[0] = 0; //根结点编码0
while (jd[f].parent != -1)
{
j = f;
f = jd[f].parent;
if (jd[f].lch == j) //置左分支编码0
{
j = strlen(jd[i].bits);
memmove(jd[i].bits + 1, jd[i].bits, j + 1);
//依次存储连接“0”“1”编码
jd[i].bits[0] = '0';
}
else //置右分支编码1
{
j = strlen(jd[i].bits);
memmove(jd[i].bits + 1, jd[i].bits, j + 1);/*memmove用于从src拷贝count个字节到dest,如果目标区域和源区域有重叠的话,
memmove能够保证源串在被覆盖之前将重叠区域的字节拷贝到目标区域中。*/
jd[i].bits[0] = '1';
}
}
}
/*哈夫曼树从根到每个叶子都有一条路径,
对路径上的各分支约定指向左子树分支编码为0,右子树分支编码为1,
从根到每个叶子相应路径上的0和1组成的序列就是这个叶子节点的编码。
从叶子结点出发去判断分支编码为0或为1,当到达根结点时编码结束。
初始时字符数组bits只有一个结束符。可通过for循环依次对n个叶子结点编码,
在for循环里面先对AscII码为0的字符,编码数组即为\0结束符,置左分支编码0,
置右分支编码1,拷贝,留出位置放当前的编码,通过拷贝把\0复制,
memmove不出现重叠部分被覆盖,再依次存储连接"0" "1"编码。
*/
printf("\n\t文本中所有出现字符编写的01序列\n");
int ans=0;
for (i = 0; i < 512; ++i)
{
if (strcmp("", jd[i].bits) != 0)
{
printf("\t%2ld.%c -> %s\n", i+1,char(jd[i].b), jd[i].bits);
ans++;
}
}
printf("\t文本中共出现%ld个字符,出现过%d种字符\n", flength,ans);
fseek(ifp, 0, SEEK_SET); //从文件开始位置向前移动0字节,即定位到文件开始位置
fwrite(&flength, sizeof(int), 1, ofp);
/*用来将数据写入文件流中,参数flength指向欲写入的数据地址,
总共写入的字符数以参数size*int来决定,返回实际写入的int数目1*/
fseek(ofp, 8, SEEK_SET);
buf[0] = 0; //定义缓冲区,它的二进制表示00000000
f = 0;
pt1 = 8;
/*假设原文件第一个字符是"A",8位2进制为01000001,编码后为0110识别编码第一个'0',
那么我们就可以将其左移一位,看起来没什么变化。下一个是'1',应该|1,结果00000001
同理4位都做完,应该是00000110,由于字节中的8位并没有全部用完,我们应该继续读下一个字符,
根据编码表继续拼完剩下的4位,如果字符的编码不足4位,还要继续读一个字符,
如果字符编码超过4位,那么我们将把剩下的位信息拼接到一个新的字节里*/
while (!feof(ifp))
{
c = fgetc(ifp);
/*在文件处理中,通过fgetc()函数,我们从输入流中获取下一个字符,并将文件指针加1。
函数fgetc()的原型是: int fgetc(FILE * filename);
它返回一个整数值,该值是无符号char的转换。 它还返回EOF ,而EOF也是一个整数值。*/
f++;
for (i = 0; i < n; i++)
{
if (c == jd[i].b) break;
}
strcat(buf, jd[i].bits);
j = strlen(buf);
c = 0;
while (j >= 8) //对哈夫曼编码位操作进行压缩存储
{
for (i = 0; i < 8; i++)//字符的有效存储不超过8位,则对有效位数左移实现补0
{
if (buf[i] == '1') c = (c << 1) | 1;
else c = c << 1;
}
fwrite(&c, 1, 1, ofp);
pt1++; //统计压缩后文件的长度
strcpy(buf, buf + 8); //一个字节一个字节拼接
j = strlen(buf);
}
if (f == flength) break;
}
if (j > 0) //对哈夫曼编码位操作进行压缩存储
{
strcat(buf, "00000000");
for (i = 0; i < 8; i++)
{
if (buf[i] == '1') c = (c << 1) | 1;
else c = c << 1;
}
fwrite(&c, 1, 1, ofp);
pt1++;
}
fseek(ofp, 4, SEEK_SET);//移动指针位置到第4个字节
/*意思是把文件指针指向文件的开头
fseek 函数名: fseek 功 能: 重定位流上的文件指针
用 法: int fseek(FILE *stream, long offset, int fromwhere);
描 述: 函数设置文件指针stream的位置。如果执行成功,stream将指向以fromwhere为基准。
*/
fwrite(&pt1, sizeof(long), 1, ofp);//写入统计后文件长度
fseek(ofp, pt1, SEEK_SET);//移动文件指针到文件尾
fwrite(&n, sizeof(long), 1, ofp);//写入节点数目,总的不同字节的个数
for (i = 0; i < n; i++)
{
fwrite(&(jd[i].b), 1, 1, ofp);//写入每个节点代表的字符
c = strlen(jd[i].bits);
fwrite(&c, 1, 1, ofp);
j = strlen(jd[i].bits);//统计赫夫曼长度
if (j % 8 != 0) //若存储的位数不是8的倍数,则补0
{
for (f = j % 8; f < 8; f++)
strcat(jd[i].bits, "0");
}
/*当若文件读完,buf中的内容可以不足一个字节,需要补0,
保证每次写入都是字节的n倍,再对哈夫曼编码位操作,
把最后一个字节写入压缩文件。解压缩时可通过记录下来的编码长度无误截断*/
while (jd[i].bits[0] != 0)//将赫夫曼字符串变成二进制数字
{
c = 0;
for (j = 0; j < 8; j++) //字符的有效存储不超过8位,则对有效位数左移实现两字符编码的连接
{
if (jd[i].bits[j] == '1') c = (c << 1) | 1; //|1不改变原位置上的“0”“1”值
else c = c << 1;
}
strcpy(jd[i].bits, jd[i].bits + 8); //把字符的编码按原先存储顺序连接
fwrite(&c, 1, 1, ofp);
}
}
length2 = pt1--;//压缩后的文件大小
div = ((double)length1 - (double)length2) / (double)length1; //计算文件的压缩率
fclose(ifp);//关闭文件
fclose(ofp);
printf("\n\t压缩文件成功!\n");
printf("\t压缩率为 %f%%\n\n", div * 100);
return;
}
/*解压缩*/
void unys()
{
char name[255], outname[255], buf[255], bx[255];
//buf是缓冲区用来保存字节的赫夫曼代码
unsigned char c;
long i, j, m, n, f, p, l;
long flength;
FILE *ifp, *ofp;
printf("\t请您输入需要解压缩的文件(包括拓展名):");
gets(name);
ifp = fopen(name, "rb");
if (ifp == NULL)
{
printf("\n\t文件打开失败!\n");
return;
}
printf("\t请您输入解压缩后的文件(包括拓展名):");
gets(outname);
ofp = fopen(outname, "wb");
if (ofp == NULL)
{
printf("\n\t解压缩文件失败!\n");
return;
}
fread(&flength, sizeof(long), 1, ifp); //读取原文件长度,对文件进行定位
fread(&f, sizeof(long), 1, ifp);
fseek(ifp, f, SEEK_SET);
fread(&n, sizeof(long), 1, ifp);
for (i = 0; i < n; i++)
{
fread(&jd[i].b, 1, 1, ifp);
fread(&c, 1, 1, ifp);
p = (long)c; //读取原文件字符的权值
jd[i].count = p;
jd[i].bits[0] = 0;
if (p % 8 > 0) m = p / 8 + 1;
else m = p / 8;
for (j = 0; j < m; j++)
{
fread(&c, 1, 1, ifp);
f = c;
itoa(f, buf, 2); //将f转换为二进制表示的字符串
f = strlen(buf);
for (l = 8; l > f; l--)
{
strcat(jd[i].bits, "0");
}
strcat(jd[i].bits, buf);
}
jd[i].bits[p] = 0;
}
/*通过循环构造Huffman树的n个叶子结点,每读取一个字节,得到huffman树的一个节点,字符对应的哈夫曼编码长度后,
再从中每次取出一个字节,转换为二进制表示的字符串。要补0直到凑够8位。*/
for (i = 0; i < n; i++) //根据哈夫曼编码的长短,对结点进行排序
{
for (j = i + 1; j < n; j++)
{
if (strlen(jd[i].bits) > strlen(jd[j].bits))
{
tmp = jd[i];
jd[i] = jd[j];
jd[j] = tmp;
}
}
}
p = strlen(jd[n - 1].bits);
fseek(ifp, 8, SEEK_SET);
m = 0;
bx[0] = 0;
while (1) //通过哈夫曼编码的长短,依次解码,从原来的位存储还原到字节存储
{
while (strlen(bx) < (unsigned int)p)
{
fread(&c, 1, 1, ifp);
f = c;
itoa(f, buf, 2);
f = strlen(buf);
for (l = 8; l > f; l--) //在单字节内对相应位置补0
{
strcat(bx, "0");//har *strcat(char *dest, const char *src) 把 src 所指向的字符串追加到 dest 所指向的字符串的结尾。
}
strcat(bx, buf);
}
for (i = 0; i < n; i++)
{
if (memcmp(jd[i].bits, bx, jd[i].count) == 0) break;
}
strcpy(bx, bx + jd[i].count); /*从压缩文件中的按位存储还原到按字节存储字符,
字符位置不改变,是将一个字符串复制到另一块空间地址中 的函数*/
c = jd[i].b;
fwrite(&c, 1, 1, ofp);//把字符写到解压缩的文件里
m++; //统计解压缩后文件的长度
if (m == flength) break; //flength是原文件长度
}
/*先根据编码长度将结点排序,先求出最长的Huffman编码。
当取出的字符个数大于等于最长的编码长度,保证可以转换。
这里利用排序算法将前n个结点排序,编码短的在前面,
最长的编码在jd.bit数组下标为n-1的位置。
定位文件指针,读取压缩文件中原文件的对应的Huffman编码信息。
每次读取一字节转成8个“01”字符后暂存在字符数组bx中,
直到bx的长度大于等于huffman编码的最大长度,
才将相应的编码转化成相应的字节值。这里读取一字节利用itoa函数转换,
如长度不足8位要在前面补足0。
*/
fclose(ifp);
fclose(ofp);
printf("\n\t解压缩文件成功!\n");
if (m == flength) //对解压缩后文件和原文件相同性比较进行判断(根据文件大小)
printf("\t解压缩文件与原文件相同!\n\n");
else printf("\t解压缩文件与原文件不同!\n\n");
return;
}
/*主函数*/
using namespace std;
int main()
{
printf("\t_______________________________________________\n");
printf("\n");
printf("\t * 解压缩程序——菜单 * \n");
printf("\t_______________________________________________\n");
printf("\n");
printf("\t 说明:仅适用于txt文本文件\n");
int c;
while (1)
{
printf("\t_______________________________________________\n");
printf("\t| |\n");
printf("\t| 1.压缩 |\n");
printf("\t| 2.解压 |\n");
printf("\t| 0.关闭 |\n");
printf("\t|_____________________________________________|\n");
printf("\n");
do
{
printf("\n\t*请选择相应功能(0-2):");
c = getch();
printf("%c\n", c);
if (c != '0' && c != '1' && c != '2')
{
printf("\t输入有误!请输入0-2之间的数字\n");
printf("\t请再输入一遍!\n");
}
} while (c != '0' && c != '1' && c != '2');
if (c == '1') ys(); //调用压缩函数
else if (c == '2') unys(); //调用解压函数
else
{
printf("\t感谢使用~\n");
exit(0); //退出
}
system("pause"); //任意键继续
}
return 0;
}
测试数据运行结果如下:
_______________________________________________
* 解压缩程序——菜单 *
_______________________________________________
说明:仅适用于txt文本文件
____________________________________________
| |
| 1.压缩 |
| 2.解压 |
| 0.关闭 |
|____________________________________________|
*请选择相应功能(0-2):1
请您输入需要压缩的文件:a.txt
请您输入压缩后的文件:aa.zip
文本中所有出现字符编写的01序列
1.a -> 1
2.w -> 010
3.d -> 001
4.p -> 000
5.s -> 0111
6.b -> 01101
7.v -> 0110001
8.c -> 0110000
9.B -> 011001000
10.C -> 011001001
11.D -> 011001010
12.i -> 011001011
13.E -> 011001100
14.F -> 011001101
15.G -> 011001110
16.A -> 011001111
文本中共出现9630个字符,出现过16种字符
压缩文件成功!
压缩率为 79.013499%
请按任意键继续. . .
____________________________________________
| |
| 1.压缩 |
| 2.解压 |
| 0.关闭 |
|____________________________________________|
*请选择相应功能(0-2):2
请您输入需要解压缩的文件:aa.zip
请您输入解压缩后的文件:aaa.txt
解压缩文件成功!
解压缩文件与原文件相同!
请按任意键继续. . .
____________________________________________
| |
| 1.压缩 |
| 2.解压 |
| 0.关闭 |
|____________________________________________|
*请选择相应功能(0-2):0
感谢使用~
--------------------------------
Process exited after 167.5 seconds with return value 0
请按任意键继续. . .
EXE结果截图如下
生成压缩解压缩文件如下:
原文本文件和压缩解压缩之后生成的文本文件对比图:
七、实验结果分析:
在该实验的完成中耗时颇多,出现过不少问题,也有了不少收获。
以下:
我希望执行的功能是选择解压缩的基础上,更够更简洁方便,更人性化,最好显示一些统计数据,因此我加入了一些文件名称和统计字符的代码。,不过能让这个代码变得更贴合。在代码的解读方面,我习惯从运行过程的角度,从主函数开始模拟电脑的运行,这样是最快捷了解一个程序的方式,删去了一些我觉得多余的地方,为了以防万一,我会有所备份,以及在更改之后会运行一下,对比看看这个更改是否正确。当然这没什么技术含量
问题:
其实我觉得这也是每个人的问题,是否对赫夫曼树有足够的了解,甚至对于文件读写运用的流程脉络是否清晰,对于整个程序追求的效果以及它是如何实现的是否领会。
解决方法:
我自认为我对文件读写等C语言方面的理论较为清楚,但是我对建树存树这方面理解不够到位,所以我主要去学习的就是关于这方面。我仔细看了教科书上的内容,以及去互联网上查阅关于赫夫曼树的一些讲解,通过结合代码段,使我可以更加掌握该方面的知识。
收获:
在完成一个较为完整的代码的过程也是一个自我检验的过程,在理解实验要求的方面是否出现偏差,在解读代码的时候是否吃力,是否能更独立的修改调试甚至做出一个优美的程序工具,这都是对我们能力的不断校验。从这个方面,我的欠缺还有很多。这个过程,发现了不少还搞不懂还没学会的地方,不仅当场解决了一些小问题,还更清楚了之后要加强学习的方面,希望日后我能不用参考别人的代码,独立完成自己想要的程序功能。