数据结构上机实验,老师要求构建哈夫曼树哈夫曼编码,输出字符串的哈夫曼编码形式,以及根据一段密文根据哈夫曼编码还原成字符串。
整体思路如下
头文件
#include <stdio.h>
#include <string.h>
#define N 50
#define M 2*N-1
主函数部分
int main()
{
int i;
char str[N] = "\0"; //字符数组初始化\0
printf("请输入字符串:");
gets_s(str);
HTNode ht[N] = { 0 };
HCode hcd[N] = { 0 };
int count[127] = { 0 };
toal(str,count);
int n = 0;//统计字母出现的种类
for (i = 0; i < 127; i++)//字符和权值存放进哈夫曼结点中
{
if (count[i] != 0)
{
ht[n].data = (char)i;
ht[n].weight = count[i];
n++;
}
}
InitHT(ht, n);//初始化
CreateHT(ht, n);//创建哈夫曼树
CreateHCode(ht, hcd, n);//创建哈夫曼编码
DispHcode(ht, hcd, n);//输出哈夫曼编码
EnCryption(str, ht, hcd, n);//对字符进行密文输出
DeCryption(ht, n);
return 1;
}
统计字符和字符出现的次数
我们用字符出现的来表示哈夫曼树的权重,因为是字符我们可以定义一个数组,再把字符转换成整型,就是字符对应的ASCII码,在数组对应的位置加一,代码如下:
void toal(char str[],int count[])
{
int i = 0;
int j = 0;
int num;
int n = strlen(str);//字符长度
for (i = 0; i < n; i++)//统计每个字符出现的频度
{
num = str[i];
count[num - 0]++;
}
for (i = 0; i < 127; i++) //输出字符和字符出现的频度
{
if (count[i] != 0)
{
printf("字符: %c,权值: %d\n", i, count[i]);
}
}
return;
}
哈夫曼树树的结点类型
想要构建哈夫曼树首先要确定哈夫曼树的结点类型。哈夫曼树的结点类型的内容包括结点值、权重双亲结点、左孩子结点、右孩子结点;所以我们的结点类型设计如下:
typedef struct
{
char data;
int weight;
int parent;
int lchild;
int rchild;
}HTNode;
之后我们还需要定义一个结点来表示并存储哈夫曼编码,首先需要一个数组来存放结点的哈夫曼编码,还需要一个值来记录那部分是某个字符的哈夫曼编码,所以我们的结点类型定义如下:
typedef struct
{
char cd[N];
int start;
}HCode;
哈夫曼树的构造算法
定理“对于具有n个叶子结点的哈夫曼树,共有2n-1个”
首先定义一个哈夫曼结点类型的ht[ ]数组来存放哈夫曼树。算法思路:n个叶子结点只有data和weight域值,首先初始化哈夫曼树,先将2n-1个结点的parent、lchild、rchild域值置为初值-1。
代码实现如下:
void InitHT(HTNode ht[], int n)
{
int i;
for (i = 0; i < 2 * n - 1; i++)
{
ht[i].parent = -1;
ht[i].lchild = -1;
ht[i].rchild = -1;
}
printf("\n 初始化哈夫曼树:\n");
printf("-----------------------\n");
printf("下标 data weight lchild rchild parent\n");
printf("-----------------------\n");
for (i = 0; i < n; i++)
{
printf("%d %c %d %d %d %d\n", i, ht[i].data, ht[i].weight, ht[i].lchild, ht[i].rchild, ht[i].parent);
}
printf("-----------------------\n");
}
然后我们在处理每个非叶子结点ht[ i ](在ht[ i ]~ht[ 2n-2 ]中);从非叶子结点中找出根节点(parent值为初值-1)最小的两个结点,ht[ lnode] 和 ht[ rnode ],将他们作为ht[ i ]的左右子树,将ht[ lnode ] 和ht[ rnode ]的双亲结点置为ht[ i ],并且ht[ i ].weight = ht[ lnode].weight + ht[ rnode ].weight。重复这几步,知道n-1个非叶子结点处理完毕,根据思路来写代码,代码如下:
void CreateHT(HTNode ht[], int n)
{
int i, k, lnode, rnode;
int min1, min2;
for (i = n; i < 2 * n - 1; i++)
{
min1 = min2 = 32767;
lnode = rnode = -1;
for (k = 0; k <= i - 1; k++)//找到最小和次小的结点
{
if (ht[k].parent == -1)
{
if (ht[k].weight < min1)
{
min2 = min1;
rnode = lnode;
printf(" %d ", rnode);
printf(" %d ", lnode);
min1 = ht[k].weight;
lnode = k;
}
else if (ht[k].weight < min2)
{
min2 = ht[k].weight;
rnode = k;
}
}
}
ht[lnode].parent = i;
ht[rnode].parent = i;
ht[i].weight = ht[lnode].weight + ht[rnode].weight;
ht[i].lchild = lnode;
ht[i].rchild = rnode;
}
printf("\n 显示建立的哈夫曼树:\n");
printf("-----------------------\n");
printf("下标 data weight lchild rchild parent\n");
printf("-----------------------\n");
for (i = 0; i < 2 * n - 1; i++)
{
printf("%d %c %d %d %d %d\n", i, ht[i].data, ht[i].weight, ht[i].lchild, ht[i].rchild, ht[i].parent);
}
printf("-----------------------\n");
}
哈夫曼编码
我们规定哈夫曼树中的左分支为0,右分支为1,根据根节点到每个字符所在的结点所经过的分支对应的0和1组成的序列就是我们的哈夫曼编码。将对应的哈夫曼编码hcd[ i ]的start域值置为初值n,找到双亲结点ht[ f ],若当前结点是双亲结点的孩子结点,则在hcd[ i ]的cd数组添加‘0’,是右孩子添加‘1’,并将start-1。再对双亲结点进行同样的操作,直到无双亲结点,所以start指向哈夫曼编码最开始的字符。代码实现如下:
void CreateHCode(HTNode ht[], HCode hcd[], int n)
{
int i, f, c;
HCode hc;
for (i = 0; i < n; i++)
{
hc.start = n;
c = i;
f = ht[i].parent;
while (f != -1)
{
if (ht[f].lchild == c)//左孩子存放字符0
hc.cd[hc.start--] = '0';
else
hc.cd[hc.start--] = '1'; //右孩子存放字符1
c = f;
f = ht[f].parent;
}
hc.start++;
hcd[i] = hc;
}
}
哈夫曼编码的输出
想知道哈夫曼编码是什么样的,就要输出一下。构造哈夫曼编码的时候知道start的值是哈夫曼编码最开始的字符,且哈夫曼编码数组与哈夫曼树数组的值顺序一一对应,所以代码如下:
void DispHcode(HTNode ht[], HCode hcd[], int n)
{
int i, k;
int sum = 0, m = 0, j;
printf("\n 输出哈夫曼编码:\n");
printf("-----------------------\n");
for (i = 0; i < n; i++)
{
j = 0;
printf(" %c:\t", ht[i].data);
for (k = hcd[i].start; k <= n; k++)
{
printf("%c", hcd[i].cd[k]);
j++;
}
printf("\n");
m += ht[i].weight;
sum += ht[i].weight * j;
}
printf("-----------------------\n");
printf("\n平均长度=%g\n", 1.0 * sum / m);
}
输出密文
前面说了哈夫曼编码与哈夫曼树一一对应,所以我们只要找到字符与哈夫曼树的data值域相等,然后就能找到对应的哈夫曼编码,最后输出就可以了。代码实现如下:
void EnCryption(char c[], HTNode ht[], HCode hcd[], int n)
{
int i = 0;
int j = 0;
int k = 0;
int num = strlen(c);
printf("输入的字符为:%s\n", c);
printf("字符个数为%d\n", num);
printf("该文对应的密文为:\n");
for (i = 0; i < num; i++)//遍历数组
{
for (j = 0; j < n; j++)//遍历哈夫曼树
{
if (c[i] == ht[j].data) //寻找字符对应的哈夫曼编码
{
for (k = hcd[j].start; k <= n; k++)
{
printf("%c", hcd[j].cd[k]);
}
}
}
}
printf("\n加密完成\n");
}
根据哈夫曼编码进行解密
在构造哈夫曼编码的时候说了往左为0,往右为1,所以我们每次从根节点开始找,遇见0就往左走,遇见1就往右走,直到找到叶子结点。根据前面定理可以知道跟结点的是2n-1,但是这里要注意了,因为我们的数组下标是从0开始的所以我们每次初始化根节点的位置是2n-2。代码如下:
void DeCryption(HTNode ht[], int n)
{
printf("请输入一组0和1字符串:\n");
char c1[N] = "\0"; //字符数组初始化\0
gets_s(c1);
int length1 = strlen(c1);
printf("\n密文长度为%d", length1);
int i = 0;
int node;
HTNode* tmp = ht;
node = 2 * n - 2;//记录结点的位置,初始化为根节点位置
printf("\n对输入的密文进行解密,解密后的结果为:\n");
while (i < length1)
{
while (tmp[node].lchild != -1 || tmp[node].rchild != -1) //寻找叶节点,找到叶节点推出循环
{
if (c1[i] == '0')//根据定义哈夫曼编码,0左 右1
{
node = tmp[node].lchild;
}
else
{
node = tmp[node].rchild;
}
i++;
}
printf("%c", tmp[node].data);
node = 2 * n - 2;//再次把记录节点的位置初始化为根节点
}
printf("\n解密完成 欢迎下次再来!!!");
}
第一次学哈夫曼树的,学的还不好,多多见谅!!!