问题描述:输入一串字符串,根据给定的字符串中字符出现的频率建立相应的哈夫曼树,构造哈夫曼树编码表,在此基础上可以对压缩文件进行压缩(即编码),同时可以对压缩后的二进制编码文件进行解码(即译码)。
输入要求:多组数据,每组数据1行,为一个字符串(只考虑26个小写字母即可)。当输入字符串为“0”时,输入结束。
输出要求:每组数据输出2n+3行(n为输入串中字符类别的个数)。第一行为统计出来的字符出现频率(只输出存在的字符,格式为:字符:频度),每两个字符之间用一个空格分隔,字符按照ASCII码从小到大的顺序排列。第2行至第2n行为哈夫曼树的存储结构的终态(一行中的数据用空格分隔)。第2n+1行为每个字符的哈夫曼编码(只输出存在的字符,格式为:字符:编码),每两组字符之间用一个空格分隔,字符按照ASCII码从小到大的顺序排列。第2n+2行为编码后的字符串,2n+3行为解码后的字符串(与输入的字符串相同)。
输入样例:
aaaaaaabbbbbccdddd
aabccc
0
输出样例:
a:7 b:5 c:2 d:4
1 7 7 0 0
2 5 6 0 0
3 2 5 0 0
4 4 5 0 0
5 6 6 3 4
6 11 7 2 5
7 18 0 16
a:0 b:10 c:110 d:111
00000001010101010110110111111111111
aaaaaaabbbbbccdddd
a:2 b:1 c:3
1 2 4 0 0
2 1 4 0 0
3 3 5 0 0
4 3 5 2 1
5 6 0 3 4
a:11 b:10 c:0
111110000
aabccc
一、相关算法分析
(一)大致思路:
题目中要求构建哈夫曼数,则需要统计字母的权数(即出现次数),又因为在“输出字母和对应出现次数”以及“输出字母和对应的编码”的两个操作中,都要求“以ASCII码从小到大顺序”输出,所以想到用一个专门的变量来存放字母和出现次数的信息,并且对这些信息以ASCII码从小到大进行排序操作。
对于译码操作,首先要明确译码的对象,即对输入字符串的编码进行译码操作。那么对于如何获得译码操作对象,不妨在输出编码的同时用将编码存入字符串encode中,则在译码操作中对已获得encode数组中的字符串进行译码即可。
(说明:)方便起见,字母信息表CH、哈夫曼树HT、存放编码的数组HC都从下标1开始使用。
程序主要用到的变量及函数如下:
1.字符数组str[100]存放输入的字符串。
2.顺序表CH,用以存放不同的字母的信息,数据元素包括字母和相应出现的次数两个数据项。
3.排序函数Arrange,用以将表CH中信息按字母从小到大顺序输出。
4.选择函数,选出无双亲节点中权值最小的两个结点的下标。
5.编码和译码函数。
6.找编码函数,对当前截取的编码是否存在对应的字符进行判断,并返回对应字符在CH表中的下标。
(二)相关算法描述:
1.输入多组数据
定义一个足够大的二维字符串数组,用以存放每组字符串,直到输入的字符串为"0"时结束。若当前字符串不为"0"(即当前字符串首元素不为'0'),在循环中完成:将字符串赋给一个一维字符数组、字母及个数输出、哈夫曼编码输出等一系列操作。
2.构建字母信息表CH并排序
构造一个空的顺序表CH,为其分配一个大小为27的数组空间(小写字母有26个,数组从下标1开始存入数据),空表长为0,数据元素由字母及出现次数count两个数据项组成。在Put函数中,依次遍历字符串中的字母,每遍历一个字母,都在表CH中进行一次查找,若未找到则为新字母,存入表CH中,count置为1,若找到则不是新字母,使该字母次数count加1。在Put函数中具体完成以下操作:
1)整型变量i为字符串数组下标,赋初值为0。
2)当前字符不为'0'时,在循环中完成以下操作:
[1] 定义整型变量j并赋初值为1,j用以表示当前表CH的数组元素下标;定义p指向表CH存放的第一个元素。
[2] 在表CH中查找当前字母。当j小于等于当前表长时,在循环中完成以下操作:
Ø 若p当前指向字母为要找的字母,则字母次数加1,结束循环;否则p指向表中下一个元素,j加1;
Ø 若j大于表长,则说明未找到该字母,将新字母存入表中,次数设为1,表长加1。
[3] 判断下一个字符,即i加1。
3)在Arrange函数中按字母ASCII码从小到大对表CH元素排序,采用选择法。
3.构造哈夫曼树函数
1) 初始化:动态申请2n(n为不同字母的个数,即CH表长)个单元;然后循环2n-1次,从一号单元开始,依次将1至2n-1所有单元中双亲、左孩子、右孩子的下标都初始化为0;最后再循环n次,输入前n个单元中叶子节点的权值,即各字母的次数。可见权值所在单元下标与对应CH中字母所在单元下标相同。
2) 创建树:循环n-1次,通过n-1次的选择、删除与合并来创建哈夫曼树。i为当前未存放数据的单元下标,取值范围为n+1——2n-1,每完成一次循环i加1,当i小于等于2n-1时,在循环中完成下列操作:
[1]选择:定义整型变量s1,s2;调用Select函数,将当前森林中双亲为0且权值最小的两个树根结点下标赋给s1,s2;
[2]删除:s1,s2单元的双亲域赋值为当前结点下标i;
[3]合并:当前结点的左孩子为s1,右孩子为s2,权值为s1与s2权值之和。
4.选择最小权值函数
需满足最小权值元素无双亲,以及s1,s2不同。
1) 确定s1:通过循环找到当前森林中首个无双亲的元素下标并赋值给s1;从s1+1开始,寻找无双亲且权值比s1小的下标,赋给s1。
2) 为保证s2与s1不同,先将s1的双亲置为1。
3) 确定s2:通过循环找到当前森林中首个无双亲的元素下标并赋值给s2;从s2+1开始,寻找无双亲且权值比s1小的下标,赋给s2。
4) 将s1的双亲重新置为0。
5.编码函数
定义typedef char **HuffmanCode,各字符的哈夫曼编码存储在由HuffmanCode定义的动态分配的数组HC中,从1号单元开始使用,数组长度为n+1。因为每个字符编码的长度事先不能确定,所以不能预先为每个字符分配大小合适的存储空间。为不浪费存储空间,动态分配一个长度为n的(由哈夫曼编码的特点可知,字符编码长度一定小于n)的一维数组cd,用来临时存放当前正在求解的第i(1≤i≤n)个字符的编码,当第n个字符的编码求解完后,根据数组cd字符串长度分配HC[i]的空间,然后将数组cd的编码复制到HC[i]中。
因为求解编码时是从哈夫曼树的叶子出发,向上回溯至根结点。所以对每个字符,得到的编码顺序是从右向左的,故将编码向数组cd存放的顺序也是从后向前的,即每个字符的第1个编码存放在cd[n-2]中(cd[n-1]存放字符串结束标志'0'),第2个编码存放在cd[n-3]中,依此类推,直到全部编码存放完毕。
6.输出编码函数
逐个遍历每个字母,在表CH中找到该字母的下标,此下标也是该字母的编码在编码表HC中的下标,输出对应编码,并将该编码连接在字符串encode中,为译码做准备。
7.译码函数
由哈夫曼树的特点可知,对于由n个叶子节点的哈夫曼树,每个叶子节点到根节点的路径数最多为n-1,即编码位数最多为n-1;此外,哈夫曼编码为前缀编码。
从以上两个特点想到如下思路:定义一个变量i用来表示当前截取长度。从路径数i=1开始,在编码字符串encode中截取长度为i的编码并存放在node字符串数组中;在HC中查找截取的编码,若找到,则记录下标,通过下标输出表CH中的字母,移动指针p以重新定位下次截取的起点,移动长度为当前截取长度i;若未找到,则将路径数加一再次截取编码进行查找,直到找到。重复上述操作直到所有编码都译完(即p指向数组首元素不为'0')。具体操作如下:
->使p指向数组encode;当p当前指向的首元素不为'0'时,在循环中完成以下操作:
1)定义字符串数组code[n],用以存放截取的编码;初始化i=1,表示截取的编码长度。
2)将p指向数组的前i个字母赋给code(调用strncpy函数),然后将下标为i的数组元素赋字符串结束标志'0'。
3)调用find_code函数,在HC中找code字符串。若查找成功,将下标赋给变量subscript,返回OK;若查找失败,则返回0。函数的返回值和i取值范围作为循环的条件,当查找失败时,在循环中进行以下操作:
[1]截取长度i加1;
[2]按新的截取长度将p指向数组的前i个字母赋给code(调用strncpy函数),然后将下表为i设为数组元素赋字符串结束标志'0'。
4)循环结束,即查找成功,按下标subscript在表CH输出对应字母。
5)p越过截取长度i,指向新的数组首部,即p=p+i。
二、源程序清单
//环境:devc++
#include<stdio.h>
#include<stdlib.h>
#include<iostream>
#include<string.h>
#define MAXSIZE 100
#define OK 1
#define OVERFLOW -2
typedef int Status;
typedef struct
{
char c;
int count;
}LTNode; //字母表的数据元素由字符和出现次数两个数据项组成
typedef struct
{
LTNode *letter;
int length; //字母表长度
}LTList;
typedef struct
{
int weight; //结点的权值
int parent, lchild, rchild; //结点的双亲、左孩子、右孩子的下标
}HTNode, *HuffmanTree;
typedef char **HuffmanCode; //动态分配数组存储哈夫曼编码表
Status InitLTList(LTList &CH); //初始化字母信息表
void Put(LTList &CH, char str[]);
void Arrange(LTList &CH); //排序
void CreateHuffmanTree(HuffmanTree &HT, int n, LTList CH); //构造哈夫曼树
void Select(HuffmanTree HT, int len, int &s1, int &s2); //选择两个无双亲条件下权数最小的下标
void printHuffmanTree(HuffmanTree HT, int n); //输出哈夫曼树
void CreateHuffmanCode(HuffmanTree HT, HuffmanCode &HC, int n); // 构造编码表
void printEncode(HuffmanCode HC, char str[], LTList CH, char encode[]); //编码函数
void printDecode(HuffmanCode HC, char encode[], LTList CH); //译码函数
Status find_code(char node[], HuffmanCode HC, int &subscript, int len); //在HC中寻找该编码是否存在
int main()
{
char ch[MAXSIZE][MAXSIZE]; //定义一个足够大的二维数组,用来存放输入的每组字符串
int t=0;
scanf("%s", ch[t]);
while(ch[t][0]!='0') //输入字符串,直到输入字符串为0时结束
{
t++;
scanf("%s", ch[t]);
}
t=0;
while(ch[t][0]!='0') //当某组字符串不为零时,执行以下操作
{
char str[MAXSIZE]; //定义一个数组,用来存放当前进行操作的字符串
strcpy(str, ch[t]);
t++; //t加1,为下次循环做准备
LTList CH; //初始化字母信息表,用以存放字母以及出现的次数,从下标为1开始存放
InitLTList(CH); //初始化字母信息表
Put(CH, str); //构造字母信息表
Arrange(CH); //按字母顺序从小到大重新排序
int i;
i=1;
while(i<=CH.length) //输出字母及次数
{
if(i!=1) printf(" "); //每两个信息间有一个空格
printf("%c:%d", CH.letter[i].c, CH.letter[i].count);
i++;
}
printf("n");
HuffmanTree HT;
int len=CH.length; //哈夫曼树HT的叶子节点长度即为字母信息表长度
CreateHuffmanTree(HT, len, CH); //构造哈夫曼树
printHuffmanTree(HT, 2*len-1); //输出哈夫曼树终态
HuffmanCode HC;
CreateHuffmanCode(HT, HC, CH.length); //构造编码表
i=1;
while(i<=len) //输出字母及对应编码
{
if(i!=1) printf(" "); //每两组信息间有一个空格
printf("%c:%s", CH.letter[i].c, HC[i]);
i++;
}
printf("n");
char encode[MAXSIZE]="0";
printEncode(HC, str, CH, encode);
printf("n");
printDecode(HC, encode, CH);
printf("n");
}
}
Status InitLTList(LTList &CH)
{
CH.letter = new LTNode[27];
if(!CH.letter) exit(OVERFLOW);
CH.length=0;
return OK;
}
void Put(LTList &CH, char str[])
{
int i=0;
while(str[i]!='0')
{
int j=1;
LTNode *p=CH.letter;
p++;
while(j<=CH.length)
{
if(p->c==str[i])
{
p->count++;
break;
}
else
{
p++;
j++;
}
}
if(j>CH.length) //未找到相同字母,是一个新的字母
{
p->c=str[i]; //将新字母放入新的单元
p->count=1; //新字母出现的次数设为1
CH.length=CH.length+1; //表长加1
}
i++;
}
}
void Arrange(LTList &CH) //选择法排序
{
int i, j, k; //i用于外循环,j用于内循环,k用于记录每层内循环中最小元素的下标
for(i=1; i<CH.length; i++)
{
k=i;
for(j=i+1; j<=CH.length; j++)
if(CH.letter[k].c>CH.letter[j].c) k=j;
if(k!=i)
{
LTNode temp;
temp=CH.letter[i];
CH.letter[i]=CH.letter[k];
CH.letter[k]=temp;
}
}
}
void CreateHuffmanTree(HuffmanTree &HT, int n, LTList CH)
{
if(n<=1) return;
int m=2*n-1;
int i;
HT = new HTNode[m+1];
for(i=1; i<=m; i++)
{
HT[i].parent=0;
HT[i].lchild=0;
HT[i].rchild=0;
}
for(i=1; i<=n; i++)
HT[i].weight = CH.letter[i].count;
for(i=n+1; i<=m; i++)
{
int s1, s2;
Select(HT, i-1, s1, s2);
HT[s1].parent=i; HT[s2].parent=i;
HT[i].lchild=s1; HT[i].rchild=s2;
HT[i].weight = HT[s1].weight+HT[s2].weight;
}
}
void Select(HuffmanTree HT, int len, int &s1, int &s2)
{
int i=1;
while(HT[i].parent) i++; //找到HT中首个无双亲的元素下标
s1=i;
for(i=s1+1; i<=len; i++)
if(!HT[i].parent && HT[i].weight<HT[s1].weight) s1=i;
HT[s1].parent=1; //将1双亲暂时赋值1,为了避免s1与s2相同
i=1;
while(HT[i].parent) i++;
s2=i;
for(i=s2+1; i<=len; i++)
if(!HT[i].parent && HT[i].weight<HT[s2].weight)
s2=i;
HT[s1].parent=0; //将s1双亲重新赋值0
}
void printHuffmanTree(HuffmanTree HT, int n)
{
int i;
for(i=1; i<=n; i++)
printf("%d %d %d %d %dn",i, HT[i].weight, HT[i].parent, HT[i].lchild, HT[i].rchild);
}
void CreateHuffmanCode(HuffmanTree HT, HuffmanCode &HC, int n)
{
HC=new char*[n+1]; //分配存储n个字符编码的编码表空间
char *cd=new char[n]; //分配临时存放每个字符编码的动态数组空间
cd[n-1]='0'; //编码结束符
int i;
for(i=1; i<=n; i++) //逐个字符求哈夫曼编码
{
int start=n-1; //start开始指向最后
int c=i, f=HT[i].parent; //f指向结点c的双亲结点
while(f) //从叶子节点开始向上回溯,直到根结点
{
--start; //回溯一次start向前指一个位置
if(HT[f].lchild==c) cd[start]='0'; //结点c是f的左孩子,则生成代码0
else cd[start]='1'; //结点c是f的右孩子,则生成代码1
c=f; f=HT[f].parent; //继续向上回溯
}
HC[i]=new char[n-start]; //为第i个字符编码分配空间
strcpy(HC[i], &cd[start]); //将求得的编码从临时空间cd复制到HC的当前行中
}
delete cd; //释放临时空间
}
void printEncode(HuffmanCode HC, char str[], LTList CH, char encode[])
{
int i=0;
while(str[i]!='0') //遍历每个字母,在LTList中找到对应字母下标,根据下标在HC中定位下标,输出编码
{
int temp=1;
while(CH.letter[temp].c!=str[i]) temp++;
printf("%s", HC[temp]);
strcat(encode, HC[temp]); //将译码粘到encode里
i++;
}
}
void printDecode(HuffmanCode HC, char encode[], LTList CH) //译码
{
char *p=encode; //p指向编码首部
while(p[0]!='0') //将所有编码译码
{
int i=1;
char code[CH.length];
strncpy(code, p, i);
code[i]='0';
int subcript; //用以记录该编码对应字母的下标
while(!find_code(code, HC, subcript, CH.length))
{
i++; //增加一位
strncpy(code, p, i);
code[i]='0';
}
printf("%c", CH.letter[subcript].c);
p=p+i; //使p指向当前未判断的编码的首部
}
}
Status find_code(char code[], HuffmanCode HC, int &subscript, int len)
{
for(subscript=1; subscript<=len; subscript++)
{
if(strcmp(code, HC[subscript])==0) return OK;
}
return 0;
}
三、运行结果