哈夫曼树的生成、编码、译码输出

数据结构上机实验,老师要求构建哈夫曼树哈夫曼编码,输出字符串的哈夫曼编码形式,以及根据一段密文根据哈夫曼编码还原成字符串。

整体思路如下

头文件

#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解密完成 欢迎下次再来!!!");
}

第一次学哈夫曼树的,学的还不好,多多见谅!!!

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值