7-2 哈夫曼编码译码

先看题目:


编写一个哈夫曼编码译码程序。

按词频从小到大的顺序给出各个字符(不超过30个)的词频,根据词频构造哈夫曼树,给出每个字符的哈夫曼编码,并对给出的语句进行译码。

为确保构建的哈夫曼树唯一,本题做如下限定:

(1)选择根结点权值最小的两棵二叉树时,选取权值较小者作为左子树。

(2)若多棵二叉树根结点权值相等,按先后次序分左右,先出现的作为左子树,后出现的作为右子树。

生成哈夫曼编码时,哈夫曼树左分支标记为0,右分支标记为1。

输入格式:

第一行输入字符个数n;

第二行到第n行输入相应的字符及其词频(可以是整数,与可以是小数);

最后一行输入需进行译码的串。

输出格式:

首先按树的先序顺序输出所有字符的编码,每个编码占一行;

最后一行输出需译码的原文,加上original:字样。

输出中均无空格

输入样例:

3
m1
n1
c2
10110

输出样例:

c:0
m:10
n:11
original:mnc


题目分析:

题目给出了n个字符以及他们的频率,也就是说给出了n个结点,让我们用这n个结点构建一个Huffman树,然后又给出一段哈夫曼编码,让我们在所构建的Huffman树的基础上,对该编码进行翻译。

所以这道题难点在于怎么构建这样一颗Huffman树(结点的选取、编码和译码)。

实验步骤:

根据Huffman树的性质我们可以知道,Huffman树中没有度为1得结点. 只有度为0和度为2得结点. 则一棵 有n个叶子结点得哈夫曼树共有2n-1个结点

所以题目给了n个结点(叶子结点),那么我们构建的Huffman树就会有2n-1个结点。

不妨画图看看?

 我们可以构建出这样一颗Huffman树,首先我们来创建一个树结点的结构体,每一个结点都存有:

 诶呀,图里忘记画 哈夫曼编码 这一项咯QWQ(也就是下图的char* code;)

对应的代码:

//Huffman树结点结构体
typedef struct TreeNode{
	double weight;//因为频率有可能为小数,所以用double
	char Ch;//用于存放该节点字符
	char *code;//用于存放该字符的 Huffman编码
	int parent,lchild,rchild;//存放每个结点的 父节点 以及 子结点
}TreeNode;

然后,将这些结点组合起来,形成一颗树:

 对应的代码:

//Huffman树结构体
typedef struct HFTree{
	TreeNode* data;//用堆实现该Huffman树
	int length;//该树总结点的个数
}HFTree;

好的,构建完树的结构体之后呢,我们可以先编写程序的输入(main)部分:

#include<iostream>
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<algorithm>
#define MAX_INT 1000
#define MAX_Double (numeric_limits<double>::max)()

using namespace std;

int main(){
	//输入n个字符
	int n;
	scanf("%d",&n);
	getchar();//吸收缓存中的回车符,以下同理
	
	//初始化n个 char和double类型的数组 用于存放 字符和对应的频率
	char* chNums = (char*)malloc(sizeof(char)*n);
	double* nuNums = (double*)malloc(sizeof(double)*n);
	
	//输入 字符以及相应的频率
	for(int i=0;i<n;++i){
		scanf("%c%lf",&chNums[i],&nuNums[i]);
		getchar();
	}
	
	//输入 需要译码的字符串
	char str[MAX_INT];
	scanf("%s",str);
	getchar();

    return 0;
}

好的,这样我们就拿到的输入的数据,接下来我们需要对拿到的字符和频率(也就是结点)进行Huffman树的构造咯!

首先来一个初始化函数:

//初始化哈夫曼树
//把输入的 字符 频率 字符个数 传进来
HFTree* initTree(double* weight,char* ch,int n){
	HFTree* T = (HFTree*)malloc(sizeof(HFTree));
	//根据性质,n个结点生成的Huffman树会有2*n-1个结点
	T->data = (TreeNode*)malloc(sizeof(TreeNode)*(2*n-1));
	T->length = n;
	
	//初始化每个结点,我们先给这n个结点(叶子结点)先初始化,等一下再初始化后面的n-1个(父结点)
	for(int i = 0; i < n; ++i){
        //给结点中的每一个数据项赋值
		T->data[i].Ch = ch[i];
		T->data[i].weight = weight[i];
        
        //没有父节点就默认为0
		T->data[i].parent = 0;

        //没有孩子结点就默认为-1
		T->data[i].lchild = -1;
		T->data[i].rchild = -1;
		
		//初始化每个结点的code数组
		T->data[i].code = (char*)malloc(sizeof(char)*MAX_INT);
		memset(T->data[i].code,'\0',MAX_INT);	
	}
	return T;
}

初始化之后呢,树的状态就为下图所示咯:

 下面就是创建一个Huffmna树的函数咯,但是我们还需要做一些准备:

因为在创建一颗Huffman树时,需要从所有结点之中,选择两个权值最小的结点来进行组合,所以我们需要编写一个选择最小两个结点的下标的函数:

//选择两个权值最小的结点,每次都会返回两个最小的结点
int* selectMin(HFTree* T){
	//定义两个哨兵变量,他们的值为double所表示的最大值
	//(numeric_limits<double>::max)();
	double min = MAX_Double;
	double secondMin = MAX_Double;
	//两个最小结点的下标
	int minIndex;
	int secondIndex;
	//先选择第一小的结点
	for(int i=0;i<T->length;++i){
		//只要没有父节点就可以选择
		if(T->data[i].parent == 0){
			if(T->data[i].weight < min){
				min = T->data[i].weight;
				minIndex = i;
			}
		}
	}
	//其次选择第二小的结点
	for(int i =0;i<T->length;++i){
		//没有父节点且不等于第一小的才选择
		if(T->data[i].parent == 0 && i != minIndex){
			if(T->data[i].weight < secondMin){
				secondMin = T->data[i].weight;
				secondIndex = i;
			}
		}
	}
	//因为返回两个值,所以可以使用指针
	int* res = (int*)malloc(sizeof(int)*2);
	res[0] = minIndex;
	res[1] = secondIndex;
	return res;
}

有了这玩意之后,我们就可以很开心的来写一个构建Huffmna树的函数咯!

//创建Huffman树
void createHFTree(HFTree* T,int nn){
	int* res;    //用于接收选择的两个最小结点
	int min;     //第一小结点的下标
	int secondMin;    //第二小结点的下标
	int n = T->length * 2 - 1;

    //从下标n开始往后构建父子结点
	for(int i = T->length; i < n;++i){
		T->data[i].parent = 0;
		res = selectMin(T);//选择两个最小的结点
		min = res[0];
		secondMin = res[1];

		//给父节点赋值
		T->data[i].weight = T->data[min].weight + T->data[secondMin].weight;
		T->data[i].lchild = min;
		T->data[i].rchild = secondMin;

		//给儿子们赋值
		T->data[min].parent = i;
		T->data[secondMin].parent = i;
		T->length++;
	}
	//为每个字符编码
	Hfmcode(T,nn);
}

在创建完树之后呢,我们的树就会变成这样咯:

 发现变化了嘛?

那么这个树的结构算是给我们给整出来了,但是每一个结点都还差自己的一个东西,那就是他们的Huffman编码!

根据题意:哈夫曼树左分支标记为0,右分支标记为1,那么3个叶子结点的Huffman编码就如下:

 开始编写编码器:

基本思路:

我们可以从叶子节点开始回溯,创建一个字符串用来存Huffman编码,左孩子就接‘0’ 右孩子就接‘1’  直到回溯到的结点的父亲为0时停止,拿m来举例:

最后得到的字符串为:01 这与我们的预期相反 所以我们要逆序将这个字符串加入到结点的哈夫曼编码项中,此时m这个叶子结点的code项就为:10 符合我们的预期!

//Huffman编码器
void Hfmcode(HFTree* T,int n){
	//分别给n个字符编码
	for(int k=0;k<n;k++){
	//从0号位的叶子节点开始回溯
	    int i = 0,j = 0;
	    int ch = k;
	    //记录单个字符的编码
	    char str[MAX_INT];
	    memset(str,'\0',MAX_INT);
		int parent = T->data[k].parent;
			for(i = n-1;parent != 0;i--){
			//如果该左孩子与 回溯起点index相符
				if(T->data[parent].lchild == ch){
					str[j++] = '0';
					ch = parent;
					//向上回溯
					parent = T->data[ch].parent;
				}else{
					//同上 右孩子.....
					str[j++] = '1';
					ch = parent;
					parent = T->data[ch].parent;
				}
			}
		int f = 0;
		//因为是回溯编码,所以需要反转存入结点中
		for(int s = j-1;s>=0;s--){
			T->data[k].code[f] = str[s];
			f++;
		}
	}
}

这时,我们的Huffman树就算是正式构建完毕,所有的结点都有了参数咯,那么我们不妨将它遍历出来看看!

来写一个遍历函数吧:

//递归遍历Huffman树 T->length-1为根结点
void preOrder(HFTree* T,int index){
	if(index != -1){
        //只遍历叶子结点
		if(T->data[index].lchild == -1 || T->data[index].rchild == -1)
			cout << T->data[index].Ch <<":"<<T->data[index].code<<endl;
		preOrder(T,T->data[index].lchild);
		preOrder(T,T->data[index].rchild);
		
	}
}

我们得到了这样的结果:

 嗯嗯,很好,接下来最后一件事情就是给题目给出的这一段谜语来个翻译就欧克!

再来一个解码函数:

//Huffman译码
char* Decode(char* str,int length,HFTree* T){
	int index = length - 1,ct = 0,j = 0;
	char ch;
	ch = str[0];
	char* res = (char*)malloc(sizeof(char)*MAX_INT);
	while(true){
		//当密文字符为0时向左走
		if(ch == '0'){
			index = T->data[index].lchild;
			//为1时向右走
		}else if(ch == '1'){
			index = T->data[index].rchild;
		}
		//直到遍历到叶子节点
		if(T->data[index].lchild == -1 || T->data[index].rchild == -1){
			//此时的字符值即为这一段密文的密码字符
			res[ct] = T->data[index].Ch;
			ct++;
			//回到根结点
			index = length-1;
		}
		//记录当前遍历密文的位置
		j++;
		ch = str[j];
		//当走完时直接及解码完成 退出循环
		if(j == (int)strlen(str))
			break;
	}
	return res;
}

ok大功告成,我们来看看结果吧!

okok,非常不错,直接上PTA:

 

 全过咯!

完整代码:

#include<iostream>
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<algorithm>
#define MAX_INT 1000
#define MAX_Double (numeric_limits<double>::max)()

using namespace std;

//Huffman树结点结构体
typedef struct TreeNode{
	double weight;//因为频率有可能为小数,所以用double
	char Ch;//用于存放该节点字符
	char *code;//用于存放该字符的 Huffman编码
	int parent,lchild,rchild;//存放每个结点的 父节点 以及 子结点
}TreeNode;

//Huffman树结构体
typedef struct HFTree{
	TreeNode* data;//用堆实现该Huffman树
	int length;//该树总结点的个数
}HFTree;

//初始化哈夫曼树
HFTree* initTree(double* weight,char* ch,int n){
	HFTree* T = (HFTree*)malloc(sizeof(HFTree));
	//根据性质,n个结点生成的Huffman树会有2*n-1个结点
	T->data = (TreeNode*)malloc(sizeof(TreeNode)*(2*n-1));
	T->length = n;
	
	//初始化每个结点
	for(int i = 0; i < n; ++i){
		T->data[i].Ch = ch[i];
		T->data[i].weight = weight[i];
		T->data[i].parent = 0;
		T->data[i].lchild = -1;
		T->data[i].rchild = -1;
		
		//初始化每个结点的code数组
		T->data[i].code = (char*)malloc(sizeof(char)*MAX_INT);
		memset(T->data[i].code,'\0',MAX_INT);	
	}
	return T;
}

//选择两个权值最小的结点
int* selectMin(HFTree* T){
	//定义两个哨兵变量,他们的值为double所表示的最大值
	//(numeric_limits<double>::max)();
	double min = MAX_Double;
	double secondMin = MAX_Double;
	//两个最小结点的下标
	int minIndex;
	int secondIndex;
	//先选择第一小的结点
	for(int i=0;i<T->length;++i){
		//只要没有父节点就可以选择
		if(T->data[i].parent == 0){
			if(T->data[i].weight < min){
				min = T->data[i].weight;
				minIndex = i;
			}
		}
	}
	//其次选择第二小的结点
	for(int i =0;i<T->length;++i){
		//没有父节点且不等于第一小的才选择
		if(T->data[i].parent == 0 && i != minIndex){
			if(T->data[i].weight < secondMin){
				secondMin = T->data[i].weight;
				secondIndex = i;
			}
		}
	}
	//因为返回两个值,所以可以使用指针
	int* res = (int*)malloc(sizeof(int)*2);
	res[0] = minIndex;
	res[1] = secondIndex;
	return res;
}

//Huffman编码器
void Hfmcode(HFTree* T,int n){
	//分别给n个字符编码
	for(int k=0;k<n;k++){
	//从0号位的叶子节点开始回溯
	    int i = 0,j = 0;
	    int ch = k;
	    //记录单个字符的编码
	    char str[MAX_INT];
	    memset(str,'\0',MAX_INT);
		int parent = T->data[k].parent;
			for(i = n-1;parent != 0;i--){
			//如果该左孩子与 回溯起点index相符
				if(T->data[parent].lchild == ch){
					str[j++] = '0';
					ch = parent;
					//向上回溯
					parent = T->data[ch].parent;
				}else{
					//同上 右孩子.....
					str[j++] = '1';
					ch = parent;
					parent = T->data[ch].parent;
				}
			}
		int f = 0;
		//因为是回溯编码,所以需要反转
		for(int s = j-1;s>=0;s--){
			T->data[k].code[f] = str[s];
			f++;
		}
	}
}

//创建Huffman树
void createHFTree(HFTree* T,int nn){
	int* res;
	int min;
	int secondMin;
	int n = T->length * 2 - 1;
	for(int i = T->length; i < n;++i){
		T->data[i].parent = 0;
		res = selectMin(T);
		min = res[0];
		secondMin = res[1];
		//给父节点赋值
		T->data[i].weight = T->data[min].weight + T->data[secondMin].weight;
		T->data[i].lchild = min;
		T->data[i].rchild = secondMin;
		//给儿子们赋值
		T->data[min].parent = i;
		T->data[secondMin].parent = i;
		T->length++;
	}
	
	//为每个字符编码
	Hfmcode(T,nn);
}

//递归遍历Huffman树 T->length-1为根结点
void preOrder(HFTree* T,int index){
	if(index != -1){
		if(T->data[index].lchild == -1 || T->data[index].rchild == -1)
			cout << T->data[index].Ch <<":"<<T->data[index].code<<endl;
		preOrder(T,T->data[index].lchild);
		preOrder(T,T->data[index].rchild);
		
	}
}

//Huffman译码
char* Decode(char* str,int length,HFTree* T){
	int index = length - 1,ct = 0,j = 0;
	char ch;
	ch = str[0];
	char* res = (char*)malloc(sizeof(char)*MAX_INT);
	while(true){
		//当密文字符为0时向左走
		if(ch == '0'){
			index = T->data[index].lchild;
			//为1时向右走
		}else if(ch == '1'){
			index = T->data[index].rchild;
		}
		//直到遍历到叶子节点
		if(T->data[index].lchild == -1 || T->data[index].rchild == -1){
			//此时的字符值即为这一段密文的密码字符
			res[ct] = T->data[index].Ch;
			ct++;
			//回到根结点
			index = length-1;
		}
		//记录当前遍历密文的位置
		j++;
		ch = str[j];
		//当走完时直接及解码完成 退出循环
		if(j == (int)strlen(str))
			break;
	}
	return res;
}


int main(){
	//输入n个字符
	int n;
	scanf("%d",&n);
	getchar();
	
	//初始化n个 char和double类型的数组 用于存放 字符和对应的频率
	char* chNums = (char*)malloc(sizeof(char)*n);
	double* nuNums = (double*)malloc(sizeof(double)*n);
	
	//输入 字符以及相应的频率
	for(int i=0;i<n;++i){
		scanf("%c%lf",&chNums[i],&nuNums[i]);
		getchar();
	}
	
	//输入 需要译码的字符串
	char str[MAX_INT];
	scanf("%s",str);
	getchar();
	//创建一棵Huffman树
	HFTree* T = initTree(nuNums,chNums,n);
	createHFTree(T,n);
	//遍历每个字符及其编码
	preOrder(T,T->length-1);
	//遍历解码后的密文
	cout<<"original:"<<Decode(str,T->length,T)<<endl;
	
	return 0;
}

博主的编程水平比较菜,写这一题花了挺长时间的,主要卡在了给每个字符编码,但是在写完了编码函数提交之后,第一个测试点不通过,以为是自己的思路有问题,而且PTA也不给测试点的数据,搞得不知道哪里出问题了,改了好几天,最后发现,原来是在判断最小的两个结点时,哨兵的值设置的太小,然后测试点给了一个很大的频率,导致一直没过,也是随便改改提交突然全过了,算是乱碰给碰出来了hhh,发出来给大家借鉴一下,加油!

评论 12
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Cristor404

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值