【C/C++】数据结构赫夫曼树文件压缩无偿分享~

无偿分享以前做的一些实验报告,喜欢点赞+关注(*^_^*)

源代码也附带了,大家有需要可以自己复制走~

时间久远,代码很稚嫩,适合大一大二的宝宝\( ̄︶ ̄*\)),有问题可以留言,有缘会回复

  • 实验题目

用哈夫曼编码实现文件压缩

二、实验目的

  1. 了解文件的概念。
  2. 掌握线性链表的插入、删除等算法。

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语言方面的理论较为清楚,但是我对建树存树这方面理解不够到位,所以我主要去学习的就是关于这方面。我仔细看了教科书上的内容,以及去互联网上查阅关于赫夫曼树的一些讲解,通过结合代码段,使我可以更加掌握该方面的知识。

收获:

在完成一个较为完整的代码的过程也是一个自我检验的过程,在理解实验要求的方面是否出现偏差,在解读代码的时候是否吃力,是否能更独立的修改调试甚至做出一个优美的程序工具,这都是对我们能力的不断校验。从这个方面,我的欠缺还有很多。这个过程,发现了不少还搞不懂还没学会的地方,不仅当场解决了一些小问题,还更清楚了之后要加强学习的方面,希望日后我能不用参考别人的代码,独立完成自己想要的程序功能。

  • 21
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值