哈工大数据结构、算法设计、计算机系统、软件构造等所有实验我都会发布在博客上,我会慢慢更新的,如果对你有帮助的话,可以多多关注一下。
目录
其他的实验链接
哈工大数据结构实验一 线性结构及其应用
哈工大数据结构实验1 算术表达式求值
哈工大数据结构实验2——二叉树存储结构的建立、遍历和应用
哈工大2019秋数据结构期末试题
对于本次实验,无非要解决的就是以下几个问题:
-
用什么数据结构去表示哈夫曼树
-
如何构造哈夫曼树
-
构造了哈夫曼树之后如何根据哈夫曼树将文本文件进行哈夫曼编码以及如何解码
本文将逐一阐述上述问题如何解决
- 首先,来看一下哈夫曼树的定义
定义:给定n个权值作为n个子叶节点,若树的带权路径最短,则这课树被称为哈夫曼树。 - 这里要注意的几个点有:
①只有叶节点才有权重,非叶节点不存储字符,也不带权重
②什么是带全路径?带权路径就是叶节点到根节点的路径长度乘以叶节点所带的权重。比如下面这个二叉树的带权路径为:
2x10 + 2x20 + 2x50 + 2x100 = 360
③哈夫曼树是带权路径最短的二叉树。
3. 好的,现在你已经知道哈夫曼树的定义,也了解了带权路径的定义。那么我们现在的目标就是构造出最短带权路径的二叉树。相信看到这里你已经有了构思,对于权重越大的叶节点,如果它离根节点越近,那么这个权重很大的叶节点对总的带权路径长度影响就越小。(right!)
比如对于上面的那个二叉树,对于权重为100的节点,如果我们把它放在离根节点最近的地方,50这个节点放在离根节点次近的地方,那么我们可以构造出一棵权重路径更小的二叉树。如图所示。
这棵树的权重路径为: 1x100 + 2x50 + 3x20 + 3x10 = 100 + 100 + 60 + 30 = 290 < 350。
看到这里,咱们的思路就清晰了,现在正式开始讲具体步骤。
假设有n个权重,构造出n个叶节点,n个权重分别为w1,w2…wn,哈夫曼树的构造规则如下:
- 将w1、w2、…,wn看成是有n 棵树的森林(每棵树仅有一个结点);
- 在森林中选出根结点的权值最小的两棵树进行合并,作为一棵新树的左、右子树,且新树的根结点权值为其左、右子树根结点权值之和;
- 从森林中删除选取的两棵树,并将新树加入森林;
- 重复(02)、(03)步,直到森林中只剩一棵树为止,该树即为所求得的哈夫曼树。
以{5,6,7,8,15}为例,咱们试着构造一棵二叉树。
① 首先选出权值最小的两棵树进行合并,作为一棵新树的左、右子树,且新树的根结点权值为其左、右子树根结点权值之和;也就是选出5和6,合并成11,剩下的树的节点权重为{7,8,11,15}
②在剩下的树的节点权重为{7,8,11,15},选出最小的两个权重{7,8}构造棵新树,权重为15,剩下的节点权重为{11,15,15}
③同理,在剩下的节点权重为{11,15,15}选出{11,15},构造新的节点,其权重为26,剩下的节点为{15,26}
④最后,选择{15,26}构造根节点即可。
上述构造出的 二叉树就是哈夫曼树。
正确性证明点击这里看即可,本篇不在阐述
好的,现在重新回到我们开始提出的三个问题。
1.用什么数据结构去表示哈夫曼树
首先,每个节点需要存储权重,以及左右儿子,同时,为了讲两个节点合并构造一个新的父节点,我们也需要在每个节点存储父节点。
最好的数据结构就是使用静态三叉链表。
所有节点构成一个数组,节点的父子关系可以用数组下标表示。
#define N 53 //带权重的n个叶子节点数,根据文件中字符种类的个数来确定
#define M 2*N-1 //n个叶子节点的哈夫曼树具有2*n-1个节点
typedef struct{
float weight;//权重
int lchild;//左儿子
int rchild;//右儿子
int parent;//父亲
}node;//静态三叉链表
typedef node huffman[M]; //哈夫曼树
typedef char *huffmancode[N];//存储每个字符的哈夫曼编码表
2. 如何构造哈夫曼树
我的思路是:
- 给定n个权重,那么叶节点就有n个,非叶节点n-1个。那么首先初始化n个叶节点,包括初始化叶节点的权重,左儿子、右儿子、父亲初始化为-1。父亲为-1表示一开始所有的叶节点还没有选中来构造树。
- 然后就开始选择两个叶节点来构造一个新的节点,这个节点的权重为两个儿子的权重之和。选择策略是:从所有的没有父亲的节点找出两个权重最小的节点。这个功能我在函数selectmin中实现,其中参数s1,和s2传递的是引用,在函数内改变s1和s2的值,会直接改变函数外s1和s2的值。
具体实现如下:
void selectmin(huffman &T,int k,int &s1,int &s2):选出权重最小的两个节点
void selectmin(huffman &T,int k,int &s1,int &s2){//选出两个权重最小的节点
int min=1000000,tmp=0;
for(int i=0;i<=k;i++){
if(T[i].parent==-1){
if(min>T[i].weight){
min=T[i].weight;
tmp=i;
}
}
}
s1=tmp;
min=100000;
tmp=0;
for(int j=0;j<=k;j++){
if((T[j].parent==-1)&&(j!=s1)){
if(min>T[j].weight){
min=T[j].weight;
tmp=j;
}
}
}
s2=tmp;
}
void CreatTree(huffman &T,float *w,int n) {//构造哈夫曼树
if(n<=1)
return ;
int i;
for( i=0;i<n;i++){//初始化哈夫曼树的n个叶节点并赋予权重
T[i].weight=w[i];
T[i].lchild=-1;
T[i].rchild=-1;
T[i].parent=-1;
}
for(;i<M;i++){ //初始化哈夫曼树的非叶节点
T[i].weight=0;
T[i].lchild=-1;
T[i].rchild=-1;
T[i].parent=-1;
}
for(i=n;i<M;i++){
int s1=0,s2=0;
selectmin(T,i-1,s1,s2);//选出权重最小的叶节点
T[s1].parent=i;
T[s2].parent=i;
T[i].lchild=s1;
T[i].rchild=s2;
T[i].weight=T[s1].weight+T[s2].weight;
}
}
3.哈夫曼编码
- 对于一棵哈夫曼树来说,编码方式就是:从根节点开始,往左走,就讲路径编码为0,往右走就讲路径编码为1,这样从根节点到叶节点所经过的所有01串就是对应叶节点的哈夫曼编码。如图所示,那么15:编码为0,5:编码为10,依次类推。
- 具体实现思路:对于每一个节点,都开始递归的找父亲节点(直到找到根节点停止递归),如果这个节点是父亲的右儿子,则编码为1,否则编码为0。将编码01保存在一个临时的数组cd内即可。字符串数组HT[i]存储了第i个叶节点的编码01串。
void bianma(huffman T,huffmancode &HT){//对每个叶节点进行编码
char cd[N];//临时保存每个节点的哈夫曼编码
cd[N-1]='\0';
int start,c,f;
for(int i=0;i<N;i++){
start=N-1;
c=i;
f=T[i].parent;
while(f!=-1){
if(T[f].lchild==c){
cd[--start]='0';
}
else{
cd[--start]='1';
}
c=f;
f=T[f].parent;//递归查找
}
HT[i]=(char*)malloc((N-start)*sizeof(char));
strcpy(HT[i],&cd[start]);
}
}
void yasuo(huffmancode HC,char ch[]){//将英文文件压缩为哈夫曼编码文件
fstream in,out;
in.open("hafuman.txt"); //打开英文文件
out.open("bianma.txt"); //打开要压缩的哈夫曼编码文件
char a[100000];
in.getline(a,100001);//读出英文文件的内容
int len1=strlen(a);
int len2=strlen(ch);
for(int i=0;i<len1;i++){
for(int j=0;j<len2;j++){
if(ch[j]==a[i]){
out<<HC[j];//将每个字符的哈夫曼编码输入到哈夫曼编码文件中
cout<<HC[j];
}
}
}
in.close() ;//关闭文件
out.close() ; //关闭文件
}
4.哈夫曼解码
哈夫曼解码:根据01串,从根节点开始在哈夫曼树搜索,0就往左走1就往右走,直到走到根节点为止。
void jiema(huffman T,char *ch,char test[],char *result) {
int p=M-1;//根节点
int i=0;//指示串的第i个字符
int j=0;//解码出的第j个字符
int len=strlen(test);
while(i<len){
if(test[i]=='0'){
p=T[p].lchild;
}
if(test[i]=='1'){
p=T[p].rchild;
}
if(p<N){//说明此时为叶节点
result[j]=ch[p];
j++;
p=M-1;//重新指向根节点
}
i++;
}
result[j]='\0';
}
void jiemawenjian(huffman T,char *ch){//将从文件读入的哈夫曼编码解码为英文文件
fstream in,out;
in.open("bianma.txt");
out.open("hafumanbianma.txt");
char a[100000];
in.getline(a,100001);
char c[100000] ;
jiema(T,ch,a,c);
out<<c;
// cout<<"哈夫曼编码文件中的内容为:"<<endl;
// cout<<a<<endl;
cout<<"解压为英文文件后的内容为:"<<endl;
cout<<c<<endl;
in.close();
out.close();
}
void dayincode(huffmancode HC,char ch[]){//打印哈夫曼树的叶节点对应的编码
cout<<endl;
cout<<"每个字符的哈夫曼编码为:"<<endl<<endl;
cout<<"字符"<<"\t"<<"\t"<<"哈夫曼编码"<<"\t"<<"\t"<<endl;
for(int i=0;i<N;i++){
cout<<ch[i]<<"\t"<<"\t"<<HC[i]<<"\t"<<"\t"<<endl;
}
cout<<endl;
}
void dayinshu(huffman T,char ch[]){
cout<<endl;
cout<<"节点"<<"\t"<<"\t"<<"字符"<<"\t"<<"\t"<<"权重"<<"\t"<<"\t"<<"父亲 "<<"\t"<<"\t"<<"左儿子"<<"\t"<<"\t"<<"右儿子"<<"\t"<<"\t"<<endl;
for(int i=0;i<M;i++){
if(i<N){ cout<<i<<"\t"<<"\t"<<ch[i]<<"\t"<<"\t"<<setprecision(9)<<T[i].weight<<"\t"<<"\t"<<T[i].parent<<"\t"<<"\t"<<T[i].lchild<<"\t"<<"\t"<<T[i].rchild<<"\t"<<"\t"<<endl;
}
else{
cout<<i<<"\t"<<"\t"<<"-1"<<"\t"<<"\t"<<T[i].weight<<"\t"<<"\t"<<T[i].parent<<"\t"<<"\t"<<T[i].lchild<<"\t"<<"\t"<<T[i].rchild<<"\t"<<"\t"<<endl;
}
}
}
main函数
int main(){
huffman T;
char a[10000];//读取文件中的字符个数
fstream in;
in.open("hafuman.txt");//打开文件
if(in.fail() ){//判断是否成功打开文件
cout<<"error"<<endl;
}
else{
in.getline(a,10001);//从文件读入10000个字符
}
cout<<"从文件读入的字符总数为:"<<strlen(a)<<endl;
int x=strlen(a);
int len[1000]={0};
for(int i=0;i<x;i++){//将读入的字符频数记录下来
int m=int(a[i]);
len[m]++;//字符ascall码为m的字符频数
}
int g=0;//记录字符个数
char ch[1000];//每个字符
for(int i=0;i<x;i++){
if(len[i]!=0){
ch[g]=char(i);//将字符ascall码转化为字符并保存在字符数组中
g++;
}
}
cout<<"文件中的不同的字符个数为:"<<g<<endl;
int str[1000];//字符出现的频数
int k=0;
for(int i=0;i<x;i++){
if(len[i]!=0){
str[k]=len[i];
k++;
}
}
str[k]='\0';//字符出现的频数
ch[k]='\0';
float w[1000];//字符频率
int t=strlen(ch);
cout<<"字符\t"<<"\t"<<"频数\t"<<"\t"<<"权重\t"<<"\t"<<endl;
for(int i=0;i<t;i++){
w[i]=(float(str[i]))/x;
cout<<ch[i]<<"\t"<<"\t"<<str[i]<<"\t"<<"\t"<<w[i]<<"\t"<<"\t"<<endl;
}
w[t]='\0';
in.close() ;
CreatTree(T,w,N);//构建哈夫曼树
dayinshu(T,ch); //打印哈夫曼树
huffmancode HC;//构建哈夫曼编码
bianma(T,HC);//对哈夫曼树进行编码
dayincode(HC,ch);//打印每个叶节点的编码
float sum=0.0;//哈夫曼树平均编码长度
for(int i=0;i<N;i++){
sum+=strlen(HC[i])*w[i];
}
cout<<"哈夫曼树平均编码长度为"<<sum<<endl;
cout<<"哈夫曼树的压缩率为:"<<1 - sum / 8<<endl;
int n=0;
cout<<"-------------------------------------------"<<endl;
cout<<"0.退出 |"<<endl;
cout<<"1.将英文文件进行压缩为哈夫曼编码文件并显示 |" <<endl;
cout<<"2.将哈夫曼编码文件解压为英文文件并显示 |"<<endl;
cout<<"3.比较英文文件和解压后的英文文件 |"<<endl;
cout<<"-------------------------------------------|"<<endl;
cin>>n;
while(n){
switch(n){
case 1:
cout<<"哈夫曼编码文件为:"<<endl;
yasuo(HC,ch);//将英文文件压缩为哈夫曼编码文件
break;
case 2:
jiemawenjian(T,ch);//将从文件读入的哈夫曼编码解压为英文文件
break;
case 3:
cout<<"读入的文件内容为:"<<endl;
cout<<a<<endl<<endl<<endl;
jiemawenjian(T,ch);
case 0:
n=0;
break;
default :
break;
}
cout<<"-------------------------------------------"<<endl;
cout<<"0.退出 |"<<endl;
cout<<"1.将英文文件进行压缩为哈夫曼编码文件并显示 |" <<endl;
cout<<"2.将哈夫曼编码文件解压为英文文件并显示 |"<<endl;
cout<<"3.比较英文文件和解压后的英文文件 |"<<endl;
cout<<"-------------------------------------------|"<<endl;
cin>>n;
}
5.最终结果展示
- 从文件中读入任意一篇英文文本文件,分别统计英文文本文件中各字符(包括标点符号和空格)的使用频率;
读入的英文文本文件:
-
统计各个字符的使用频率
-
根据已统计的字符使用频率构造哈夫曼编码树,并给出每个字符的哈夫曼编码(字符集的哈夫曼编码表);
-
每个字符的哈夫曼编码
-
将文本文件利用哈夫曼树进行编码,存储成压缩文件(哈夫曼编码文件);
-
计算哈夫曼编码文件的压缩率
-
将哈夫曼编码文件译码为文本文件,并与原文件进行比较。
读入的英文文本文件:
哈夫曼编码文件的译码文件: