课程设计报告
一、题目
在一个加密的应用中,要处理的信息来自下面的字符集,各个字符的相关使用频度如下:A B C D E F G H I J K L M,频度:180,64 ,13, 23, 32, 103,22,15, 47, 57,15, 31, 20;字符 N O P Q R S T U V W X Y Z ,频度:55 63 15 1 48 56 80 25 7 18 2 16 1 。现在请你编写程序实现以下的功能:
(1)运行时,由用户输入来初始化字符集的大小和相应用字符。
(2)输入一个要加密的字符串,将其加密。(哈夫曼编码1/0)
(3)输出解密字符串。
二、功能分析
(1)运行时,由用户输入来初始化字符集的大小和相应用字符。
很久很久之后,才明白,所谓的初始化字符集原来就是初始化编码表。换句话说,就是通过构造一棵哈弗曼树来构造编码表。再换句话说,没有构造哈弗曼树就别想得到编码表。所以,再简单来说,就是——构造哈弗曼树!!!!!
真是“博大精深”的题目,之前一直理解不能,结果一直下不了手。当然,错路走了不少。
(2)输入一个要加密的字符串,将其加密。
这个好理解多了,一句话,就是进行哈夫曼编码的流程。
关于这个,下文将详细讲述。
(3)输出解密字符串。
一句话,将1/0编码解码的流程。
三、分析
由题目中的“字符串加密”和“频度”两个关键词可知,只能用哈夫曼树来实现。当然,如果没有“频度”这个词的话,就另当别论了。总之,要做的事就两个:编码和解码。也就是通过编码来实现字符加密。
程序分为八个部分:
1、主函数部分;2、哈弗曼树的构建部分;3、哈夫曼编码表的构建;4、哈弗曼树编码部分;5、哈弗曼树解码部分;6、哈弗曼树的数据存储结构(线性结构);7、哈夫曼树的字符统计;8、哈弗曼树的析构(树结构的做法)。
这是哈夫曼编码都基本要求的流程,所以,哈弗曼树的编程大多大同小异。
主函数就不说了,作为一棵典型的二叉树,哈夫曼树的构造和析构都是必须的,我们没法直接设一个默认的析构函数来释放内存空间,因为它是字符的链表结构。
其次,要实现编码就必须有编码表,就好像国家间或军事上的密电,要加密或破解密电的内容就必须持有密码本,这个编码表就是这个“密码本”。于是,我们在构造哈弗曼树的过程按次序所得到的1/0排序,就是编码表。
然后就是编码和解码的部分了,其凭据都来自上面的编码表。
所以说,对于哈夫曼编码,获取编码表才是关键。
再来就是存储结构了,数据间的联系是树的基础。这么一说,结构的作用就不言而喻了。
至于字符统计,当然实际并没有担当关键的角色,但也是重要的辅助。重要性不需多言。
接下来该详细分解一下程序代码了。
先从哈夫曼树的构造讲起。
建树的关键是二次比较取值,第一次是对子树间的比较,第二次是作为最小结点之和的双亲结点拟成为子树结点放回原来的集合重新比较取值。因此出现两次循环结构。
其次,是哈弗曼树的析构部分。
这里分两步进行,一步是删除哈弗曼树结点,一步是删除编码表(因为编码表是伴随树而存在的)。
两者的共同点在于都是通过将值赋予代替自己被删除的结构体指针,而自己则充当虚定位指针后移,来得到内存释放和空间范围缩小的目的的。即形如Node * p=q;q++;delete p;
第三是创建编码表、编码和解码。
创建编码表的过程其实就是不断将双亲结点转换角色向孩子结点推进的过程。双亲结点在不断推进过程中不为空时,由根结点开始从上往下朝叶子所在的底层推进。在这过程中,在左子树则为0,右子树则为1,直到抵达叶子结点,编码的集合才作为一个元素存入数组中。
编码与解码原理相同,都只是对照着编码表进行转换。因为字符集与编码表绑定在一起,所以无论是字符相同或编码相同,都能找到对应的转化对象。
第四是数据存储结构,树的构建大多是先到先建。因此,队列会更合适。
关于代码的具体分析已经写于注释中,以上仅为原理分析,代码如下:
#include<iostream>
#include <cstring>
using namespace std;
//哈夫曼编码结点
struct Code
{
char data; //字符串
char code [100];
};
//哈夫曼树结点
struct TNode
{
int weight; //结点权值
int parent; //双亲指针
int lchild; //左孩子指针
int rchild; //右孩子指针
};
//构造树表和编码操作的类
class Huffman
{
public:
TNode*Tree; //哈夫曼树
Code*CodeTable; //哈弗曼编码表
void CreateTree(int a[],int n);//创建哈夫曼树
void CreateTable(char b[],int n);//创建编码表
void Encoding (char*s,int n);//编码
void Decoding (char*s,char*d,int n);//解码
void DestroyTree(int n);//析构
};
void Huffman::DestroyTree(int n) //n,极限
{
for(int i=0;i<2*n-3;i++)//析构哈夫曼树
{
TNode*p=Tree; //值传递,转存
Tree++; //指针后移
delete p; //删除
}
for(int j=0;j<n-5;j++)//析构编码表
{
Code*q=CodeTable;
CodeTable++;
delete q;
}
}
void Huffman::CreateTable(char b[],int n)//创建编码表,关键
{
CodeTable=new Code [n]; //与后文相似,n为动态数据存放空间(用户自定义的数据的存储范围)
for (int i=0;i<n;i++)
{
CodeTable[i].data=b[i];//生成编码表,数组字符串传递
int child=i; //结点数量赋为子树值,即子树结点下标初始化
int parent=Tree[i].parent;//赋值定义,初始化,使parent!=-1,生成双亲指针
int k=0;
/////////////////////////////////////////////////
while (parent!=-1) //定义规则
{
if(child==Tree[parent].lchild) //所有的1/0被存储在code[]一维数组中
CodeTable[i].code[k]='0';//左孩子标‘0’;
else
CodeTable[i].code[k]='1';//右孩子标‘1’;
k++; //k<n<==>i<n
child=parent; //front->next=front->next->next,child=front->next
parent=Tree[child].parent; //child->parent,孩子结点变为双亲结点,front=front->next
}
////////////////////////////////////////////////////////
CodeTable[i].code[k]='\0'; //定义最后的空间NULL被赋予'\0'
char*b=new char[k]; //申请k个字符变量空间,没有赋初值,并定义一个整型指针a指向该地址空间开始处
//k个空间,即从根到叶子的一条路线
////////一条哈夫曼编码开始构建
for(int j=0;j<k;j++) //k为字符串终值,null
{
b[j]=CodeTable[i].code[k-1-j]; //----->n-1-i,下标,code[]空间缩小或者说code[]的下标在递减,
}
/////////////////////////b[]的空间在增大,编码数组在形成,仅仅起着聚合编码字符的作用
for(int jj=0;jj<k;jj++) //b[j]即b[jj]被传递到code[]
{
CodeTable[i].code [jj]=b[jj]; //编码数组重新赋予编码表结构体的code[]数组,以便下面的参数传递
} //这是一个多维数组
}
}
//字符->code[]->b[]->HCodeTable[].code[]
void Huffman::Encoding(char*s,int n) //对字符串进行编码
{
while(*s!='\0') //数组不为空
{
for(int i=0;i<n;i++)
{
if(*s==CodeTable[i].data)
{
cout<<CodeTable[i].code;
s++;
}
}
}
cout<<endl;
}
void Huffman::Decoding(char*s,char*d,int n)//s为编码串,数组指针,将编码还原为字符串
{
while(*s!='\0')
{
int parent=2*n-1-1; //根结点在HTree中的下标,因为有2n-1个结点,根结点下标自然是(2n-1)-1
while(Tree[parent].lchild!=-1)
{
if(*s=='0')
parent=Tree[parent].lchild; //Tree[2]->next
else
parent=Tree[parent].rchild;
s++;
}
*d=CodeTable[parent].data;
cout<<*d;
d++;
}
cout<<endl;
}
void Huffman::CreateTree(int a[],int n) //创建哈夫曼树,假定哈弗曼树是完全二叉树
{
Tree=new TNode [2*n-1]; //因为右孩子必然存在,n个结点会衍生2n-1个哈弗曼树结点
for(int i=0;i<n;i++) //初始化所有哈弗曼树结点
{
Tree[i].weight=a[i]; //a[i]存储的数组变量,统计权值,初始化n个空间,相当于n棵只有根结点的树,零图
//双亲及孩子结点蓄势待发,先抢个沙发
Tree[i].lchild=-1; //哈夫曼树结点结构体数组指针初始化
Tree[i].rchild=-1;
Tree[i].parent=-1;
}
//////////////////////////////////////////////////////////////
static int k=0;//选择权值最小的两个结点,静态定义变量
int b2[1000];
static int x;
static int y;
int min=1000;
for(int j2=0;j2<n;j2++) //j2是树数组结点下标
{
if(Tree[j2].weight<min) //多个子树之和
{
min=Tree[j2].weight; //权值赋予min,x为暂时存储数据的变量
x=j2; //下标传递参数,静态存储
}
}
b2[k]=x; //得到最小值的下标x
k++;
int _min=1000;
//多次循环以获取最小值
for(int j3=0;j3<n;j3++)
{
int k2;
for( k2=0;k2<k;k2++) //以最小值下标k为上极限,
{
if(j3==b2[k2]) //b2[]为数组下标值
k2=k+2;
}
if(k2==k)
{
if(Tree[j3].weight>=Tree[x].weight)
{
if(Tree[j3].weight<_min)
{
_min=Tree[j3].weight;
y=j3;
}
}
}
}
b2[k]=y;
k++;
Tree[x].parent=Tree[y].parent=n;
Tree[n].weight=Tree[x].weight+Tree[y].weight; //权值之和
Tree[n].lchild=x;
Tree[n].rchild=y;
Tree[n].parent=-1;
//和上文相同的做法,
for(int i2=n+1;i2<2*n-1;i2++) //开始创建哈夫曼树,二次构造
{
min=1000;
for(int j2=0;j2<i2;j2++)
{
int k2;
for( k2=0;k2<k;k2++) //
{
if(j2==b2[k2])
k2=k+2;
}
if(k2==k)
{
if(Tree[j2].weight>=Tree[y].weight )
{
if(Tree[j2].weight<min)
{
min=Tree[j2].weight;
x=j2;
}
}
}
}
b2[k]=x;
k++;
int _min=1000;
for(int j3=0;j3<i2;j3++)
{
int k2; //
for( k2=0;k2<k;k2++)
{
if(j3==b2[k2])
k2=k+2;
}
if(k2==k)
{
if(Tree[j3].weight>=Tree[x].weight)
{
if(Tree[j3].weight<_min)
{
_min=Tree[j3].weight;
y=j3;
}
}
}
}
b2[k]=y;
k++;
Tree[x].parent=Tree[y].parent=i2;
Tree[i2].weight=Tree[x].weight+Tree[y].weight;
Tree[i2].lchild=x;
Tree[i2].rchild=y;
Tree[i2].parent=-1;
}
}
struct Node
{
char data;
int weight;
Node *next;
};
//队列存储,链表结构,统计哈弗曼树
class Linklist
{
public:
Linklist(){rear=new Node ;rear=rear->next;} //尾插法构造新结点,
void Construct(char s[]); //构造链表
int Getlength(); //统计哈弗曼树的子树个数
Node*rear;
};
void Linklist::Construct(char s[]) //构造链表
{
rear=new Node;
rear->next=rear; //尾插法
for(unsigned int i=0;i<strlen(s);i++) //strlen()函数统计字符串个数,但不包括结束标志'\0',
{ //strlen(s)为之前预先构造的哈弗曼树结点数
if(rear->next==rear) //指针指向自己,构造新结点,初始化
{
Node *p=new Node;
p->data=s[0];
p->weight=1;
p->next=rear->next ;//------------------ //
rear->next=p; //p结点插入到 //
rear=p; //终端结点rear之后 //
//------------------ //
}
else
{
Node *q=rear->next->next;
while(q!=rear->next )
{
if(q->data==s[i])
{
q->weight++;
break;
}
else
q=q->next;
}
if(q==rear->next )
{
Node *r=new Node;
r->data=s[i];
r->weight=1;
r->next =rear->next ;
rear->next =r;
rear=r;
}
}
}
}
int Linklist::Getlength () ////统计哈夫曼树的子树个数
{
int n=0;
Node*t=rear->next->next ;
while(t!=rear->next )
{
t=t->next ;
n++;
}
return n;
}
void main()
{
cout<<"请输入你所需要编码的字符串: "<<endl;
char s[1000]={'\0'};
gets(s); //gets(字符指针)函数,输入字符串,头文件stdio.h(c中),c++不需包含此头文件
//功能:从stdin流中读取字符串,直至接受到换行符或EOF时停止,
//并将读取的结果存放在buffer指针所指向的字符数组中。换行符不作为读取串的内容,
//读取的换行符被转换为null值,并由此来结束字符串。
Linklist aa;//定义类Linklist的对象aa
aa.Construct (s);//构造链表
int n=aa.Getlength ();//得到哈夫曼树的子树个数
int*a=new int[n];//申请n个整型变量空间,没有赋初值,并定义一个整型指针a指向该地址空间开始处
char*b=new char[n];
Node *v=aa.rear ->next ->next ;
for(int i=0;i<n;i++)//初始化哈夫曼树
{
a[i]=v->weight ;
b[i]=v->data ;
v=v->next;
}
Huffman aaa;//定义Huffman类的对象
aaa.CreateTree (a,n);//构建Huffman树
aaa.CreateTable (b,n);//创建编码表
cout<<"************************************************"<<endl;
cout<<"生成的字符编码表是******************************"<<endl;
cout<<"字符"<<"\t"<<"编码"<<"\t"<<"权值"<<endl;
for(int ii=0;ii<n;ii++)//打印编码表
{
cout<<aaa.CodeTable [ii].data <<"\t"<<aaa.CodeTable[ii] .code<<"\t"<<a[ii]<<endl;
}
aaa.Encoding (s,n); //编码
char ss[1000]={'\0'};
char d[1000]={'\0'};
cout<<"*************************************************"<<endl;
cout<<"请输入一串由0和1构成的哈夫曼编码"<<endl;
gets(ss); //1/0编码输入
cout<<"最终解码为:"<<endl;
aaa.Decoding (ss,d,n); //译码,根据字符集编码将输入的编码还原为字符串
}
四、收获、体会和不足
收获:哈夫曼编码一直都是我的弱点,关于里面的具体构造细节其实我始终处于懵懂的状态。因此,这次的设计实践加深我对哈夫曼编码的认识,这是很大的收获。
不足:这次设计中,树的构建仍旧是最大的难题,即便对树的原理有所了解。但事实上,在细节上还是知之不多,因此对程序设计造成很多困扰。尤其是树的遍历,仅仅了解四种遍历方法的原理,但对实践中的树的构造仍旧束手无策,同时在编程上有许多细节严重无知。