哈夫曼树建树编码解码

提示:昼短苦夜长,何不秉烛游!


再此声明因为构造过程中会有重复值的出现 导致构造的树 并不一定一样 也就导致解码 编码的不同

一、哈夫曼解析

1、手写哈夫曼树

在学这个之前 首先来借用使用一个图来帮助我们来理解 先来教大家怎么构建这样一个树,首先给定一系列数字, 从中选取两个最小的值来构成左右子树, 假如现在给定的就是 1 2 3 4 5 四个节点 从中选择 最小的两个值 但是两个值中小的一个作为 左子树,大的值一个作为右子树 (这个大的作为右子树 也是默认的,其实没有规定 若是你想 大的作为左子树也是可以的 但是要统一)此时你应该也可以看出 其中最小的两个值就是1和2,,选择1 2 来构成一棵树,那么根结点应该是谁呢 这里是使用两者和作为根 也就构成如图所示

请添加图片描述
然后将合并后的值3作为新结点放入序列中,删除序列中已经使用过的值 此时也就是 3 3 4 5 此时再选择两个小的
但是此时最小的两个的值是一样的 都是三 此时谁应该放在左边呢? 这里都可以 计算出来树的带权路径长度是一样的, 所以 通过这个可以使用序列尽管相同 但是构成的树 却不一定是 一样的 我认为 若是序列在初始 或者处理的过程中 若是有重复的值 也就会造成 树的不同 但是构成的树的最短带权路径长度依然是一样的
也就构成下图

请添加图片描述

重复上以上步骤继续选择两个构成 添加新合并的值 作为新结点放入序列中,然后删两个 较小的值 此时是6 4 5
再从中选取两个最小值

请添加图片描述

再填加一个值,再删两个值 6 9 此时序列中也就两个值 树成!!!

请添加图片描述

2、为什么这样构成哈夫曼树?

在解决这个问题之前,我们需要知道什么是几个概念,路径长度 带权路径长度

路径

从一个结点往下可以到达的结点之间的通路 称为路径

路径长度:

某一个路径所经过的边的数量 称为该路径的路径长度 等于经过的结点数减一

带权路径长度

将树中结点赋值给一个带有某种含义的数值,则该数值称为该结点的权 从根节点到该结点之间的路径长度与该结点的权的乘积 称为该结点的带权路径长度

树的最带权路径长度

树的带权路径长度规定为所有叶子结点的带权路径长度之和,记为WPL。

哈夫曼树

给定n个权值作为n个叶子结点,构造一棵二叉树,若该树的带权路径长度达到最小,则称该二叉树为哈夫曼树,也被称为最优二叉树 但是如何才能使得带权路径最小 根据计算方式 我们想到我们应该尽可能地让权值大的叶子结点靠近根结点,让权值小的叶子结点远离根结点,这样便能使得这棵二叉树的带权路径长度达到最小

二、构建哈夫曼树

使用之前的三叉动态链表的方式也是可以实现的 ,但是这里具体较之前的优点在哪 ,一个可能的原因是方便随机存取吧,

//哈夫曼树结点结构
typedef struct HTNode 
{
	char data; //数据,非叶节点为NULL
	double weight;//权重
	int parent;//双亲,-1表示没有双亲,即根节点
	int lchild;//左孩子,数组下标,-1表示无左孩子,即叶节点
	int rchild;//右孩子
}HTnode;

简单来说 其实也就是一个填表的过程 ,将下表填充完整
请添加图片描述
数组上方五个存放的是叶子结点下面四个存放的是非叶子节点, 其中lchild 和rchild 存储的都是数组的下标 ,也就是从0以开始的,根结点是没有父亲结点 我们使用一个-1标志,同理若是为叶子节点其中孩子结点为-1

#include<bits/stdc++.h>
#define ElemType int
#define Max 100;
using namespace std;
typedef struct HTNode{
	char data;
	double weight;
	int parent=-1;
	int lchild=-1;
	int rchild=-1;
}HTNode;
int n;
HTNode HTree[100];
void Creat(){//创建一个哈夫曼树
	cout<<"请输入你要填入字符的个数"<<endl;vector<int> Vec; 
	cin>>n;
	cout<<"请输入字符以及对应的权值"<<endl;
	for(int i=0;i<n;i++){
		cin>>HTree[i].data>>HTree[i].weight; 
		Vec.push_back(HTree[i].weight);
	}
	//上面几句完成了初始化  接下来进行造树(填表)
		for(int i=n;i<2*n-1;i++){ //将HTree数组中的剩下的填充完即可结束 
			if(Vec.size()==1) break;
			sort(Vec.begin(),Vec.end());
			//遍历来寻找最小值以及次小值在HTree中的坐标  但是要注意此时是给这两个结点找父亲 所以他们不能已经有了父亲 
			for(int j=0;j<i;j++){
				if(Vec[0]==HTree[j].weight&&HTree[j].parent==-1){//找最小值的父亲 
					HTree[j].parent=i;
					HTree[i].lchild=j;	
					break;//是为了避免重复值的问题 
				}
			} 
			for(int j=0;j<i;j++){
				if(Vec[1]==HTree[j].weight&&HTree[j].parent==-1){
					HTree[j].parent=i;
					HTree[i].rchild=j;
					break;
				}
			} 
			HTree[i].weight=Vec[0]+Vec[1];
			//成功一次之后 需要删除两个 添加一个
			Vec.erase(Vec.begin(),Vec.begin()+2);
			Vec.push_back(HTree[i].weight); 
		}  
}
void PrintHT()
{
	cout << "下标\t" << "数据\t" << "权重\t" << "双亲\t" << "左孩子\t" << "右孩子" << endl;
	for (int i = 0; i<n*2-1; i++)
	{
		cout << i << "\t" << HTree[i].data << "\t" << HTree[i].weight 
		<< "\t" << HTree[i].parent << "\t" << HTree[i].lchild << "\t" << HTree[i].rchild << endl;
	}
}
int main(){
	Creat();
	PrintHT();
	return 0;	
}

请添加图片描述

三、哈夫曼树编码

从根结点到叶子结点 往左是零 往右是一
这里不知道大家想到之前写过的一个题目 二叉树的所有路径 其中就是寻找根结点到所有叶子结点的路径,虽然这道题我们不使用所有路径的方法哈哈哈 因为每一个结点都有父亲并且叶子结点就在数组的前n个 找到之后反转一下即可 并且我们需要绑定每一个字符对应的哈夫曼编码 所以也就是需要定义一个结构体

typedef struct HFNode{
	char data;
	string str;
}HFNode;

整体代码如下

#include<bits/stdc++.h>
#define ElemType int
#define Max 100;
using namespace std;
typedef struct HTNode{
	char data;
	double weight;
	int parent=-1;
	int lchild=-1;
	int rchild=-1;
}HTNode;
typedef struct HFNode{
	char data;
	string str;
}HFNode;
int n;
HTNode HTree[100];
HFNode HFCode[100]; 
void Creat(){//创建一个哈夫曼树
	cout<<"请输入你要填入字符的个数"<<endl;vector<int> Vec; 
	cin>>n;
	cout<<"请输入字符以及对应的权值"<<endl;
	for(int i=0;i<n;i++){
		cin>>HTree[i].data>>HTree[i].weight; 
		HFCode[i].data=HTree[i].data;
		Vec.push_back(HTree[i].weight);
	}
	//上面几句完成了初始化  接下来进行造树(填表)
		for(int i=n;i<2*n-1;i++){ //将HTree数组中的剩下的填充完即可结束 
			if(Vec.size()==1) break;
			sort(Vec.begin(),Vec.end());
			//遍历来寻找最小值以及次小值在HTree中的坐标  但是要注意此时是给这两个结点找父亲 所以他们不能已经有了父亲 
			for(int j=0;j<i;j++){
				if(Vec[0]==HTree[j].weight&&HTree[j].parent==-1){//找最小值的父亲 
					HTree[j].parent=i;
					HTree[i].lchild=j;	
					break;//是为了避免重复值的问题 
				}
			} 
			for(int j=0;j<i;j++){
				if(Vec[1]==HTree[j].weight&&HTree[j].parent==-1){
					HTree[j].parent=i;
					HTree[i].rchild=j;
					break;
				}
			} 
			HTree[i].weight=Vec[0]+Vec[1];
			//成功一次之后 需要删除两个 添加一个
			Vec.erase(Vec.begin(),Vec.begin()+2);
			Vec.push_back(HTree[i].weight); 
		}  
}
void DealHFCodeString(){
	for(int i=0;i<n;i++){//对n个结点进行string的赋值 
		int Cur=i;//指向此时的第一个叶子节点 
		string Str;
		while(HTree[Cur].parent!=-1){
			if(HTree[HTree[Cur].parent].lchild==Cur){
				Str+=to_string(0);
			}
			else{
				Str+=to_string(1);
			}
			Cur=HTree[Cur].parent;
		} 
		reverse(Str.begin(),Str.end());
		HFCode[i].str=Str;
	}
}
//打印哈夫曼树
void PrintHT()
{
	cout << "下标\t" << "数据\t" << "权重\t" << "双亲\t" << "左孩子\t" << "右孩子" << endl;
	for (int i = 0; i<n*2-1; i++)
	{
		cout << i << "\t" << HTree[i].data << "\t" << HTree[i].weight << "\t" 
		<< HTree[i].parent << "\t" << HTree[i].lchild << "\t" << HTree[i].rchild << endl;
	}
}
void PrintHFC(){
	cout<<"数据\t"<<"哈夫曼编码\t"<<endl;
	for(int i=0;i<n;i++){
		cout<<HTree[i].data<<"\t"<<HFCode[i].str<<"\t"<<endl;
	} 
}
int main(){
	Creat();
	PrintHT();
	DealHFCodeString();
	PrintHFC();
	return 0;	
}

四、哈夫曼解码

简单的来说 就是给定一个01构成的字符串根据这个字符串相对应的字符,不过这里要考虑不止一个字符的情况,这里使用一个字符串来放解码的结果,若是能到叶子结点则将此时到达的叶子结点放入结果集中,然后从头继续匹配,还有就是是否会输入不正确的时候 什么时候会输入不正确呢? 输入字符串的最后一段在从根结点匹配的过程中到达不了叶子结点 即为输入不正确 若是Cur停留的是在根结点 就说明输入没有问题
下面写出代码

//哈夫曼解码
string HFDecode(){
	string Str;string result;
	cout<<"请输入一段01 构成的字符串"<<endl;
	cin>>Str; int Cur=n*2-2;
	//我们知道树根是放在数组的最后一个的位置上
	while(!Str.empty()){//字符串中为空的时候 也是要退出的 
		if(Str.front()=='1'){//向右深入
			cout<<"向右深入"<<endl; 
			Cur=HTree[Cur].rchild;
			Str.erase(Str.begin(),Str.begin()+1);
		}
		else{//向左深入 
			cout<<"向左深入"<<endl; 
			Cur=HTree[Cur].lchild;
			Str.erase(Str.begin(),Str.begin()+1);
		} 
		if(HTree[Cur].lchild==-1){//说明此时是叶子结点 将此时对应的结果放在result中 
			cout<<"将"<<HTree[Cur].data<<"放入结果集合中"<<endl; 
			result+=HTree[Cur].data;
			Cur=n*2-2;//到达叶子节点重新指向根结点 
		}
	} 
	if(HTree[Cur].parent!=-1){//遍历完成的时候若是Cur指向的不是叶子结点 输入有问题
		cout<<"此时你输入的有问题"<<endl;
		result.erase(result.begin(),result.end());
		return result;
	}
	return result;
} 

请添加图片描述

解码需要根据编码来看,若是多加一个数子 则输入错误
请添加图片描述

可运行如下

#include<bits/stdc++.h>
#define ElemType int
#define Max 100;
using namespace std;
typedef struct HTNode{
	char data;
	double weight;
	int parent=-1;
	int lchild=-1;
	int rchild=-1;
}HTNode;
typedef struct HFNode{
	char data;
	string str;
}HFNode;
int n;
HTNode HTree[100];
HFNode HFCode[100]; 
void Creat(){//创建一个哈夫曼树
	cout<<"请输入你要填入字符的个数"<<endl;vector<int> Vec; 
	cin>>n;
	cout<<"请输入字符以及对应的权值"<<endl;
	for(int i=0;i<n;i++){
		cin>>HTree[i].data>>HTree[i].weight; 
		HFCode[i].data=HTree[i].data;
		Vec.push_back(HTree[i].weight);
	}
	//上面几句完成了初始化  接下来进行造树(填表)
		for(int i=n;i<2*n-1;i++){ //将HTree数组中的剩下的填充完即可结束 
			if(Vec.size()==1) break;
			sort(Vec.begin(),Vec.end());
			//遍历来寻找最小值以及次小值在HTree中的坐标  但是要注意此时是给这两个结点找父亲 所以他们不能已经有了父亲 
			for(int j=0;j<i;j++){
				if(Vec[0]==HTree[j].weight&&HTree[j].parent==-1){//找最小值的父亲 
					HTree[j].parent=i;
					HTree[i].lchild=j;	
					break;//是为了避免重复值的问题 
				}
			} 
			for(int j=0;j<i;j++){
				if(Vec[1]==HTree[j].weight&&HTree[j].parent==-1){
					HTree[j].parent=i;
					HTree[i].rchild=j;
					break;
				}
			} 
			HTree[i].weight=Vec[0]+Vec[1];
			//成功一次之后 需要删除两个 添加一个
			Vec.erase(Vec.begin(),Vec.begin()+2);
			Vec.push_back(HTree[i].weight); 
		}  
}
//哈夫曼编码 
void DealHFCodeString(){
	for(int i=0;i<n;i++){//对n个结点进行string的赋值 
		int Cur=i;//指向此时的第一个叶子节点 
		string Str;
		while(HTree[Cur].parent!=-1){
			if(HTree[HTree[Cur].parent].lchild==Cur){
				Str+=to_string(0);
			}
			else{
				Str+=to_string(1);
			}
			Cur=HTree[Cur].parent;
		} 
		reverse(Str.begin(),Str.end());
		HFCode[i].str=Str;
	}
}
//哈夫曼解码
string HFDecode(){
	string Str;string result;
	cout<<"请输入一段01 构成的字符串"<<endl;
	cin>>Str; int Cur=n*2-2;
	//我们知道树根是放在数组的最后一个的位置上
	while(!Str.empty()){//字符串中为空的时候 也是要退出的 
		if(Str.front()=='1'){//向右深入
			cout<<"向右深入"<<endl; 
			Cur=HTree[Cur].rchild;
			Str.erase(Str.begin(),Str.begin()+1);
		}
		else{//向左深入 
			cout<<"向左深入"<<endl; 
			Cur=HTree[Cur].lchild;
			Str.erase(Str.begin(),Str.begin()+1);
		} 
		if(HTree[Cur].lchild==-1){//说明此时是叶子结点 将此时对应的结果放在result中 
			cout<<"将"<<HTree[Cur].data<<"放入结果集合中"<<endl; 
			result+=HTree[Cur].data;
			Cur=n*2-2;//到达叶子节点重新指向根结点 
		}
	} 
	if(HTree[Cur].parent!=-1){//遍历完成的时候若是Cur指向的不是叶子结点 输入有问题
		cout<<"此时你输入的有问题"<<endl;
		result.erase(result.begin(),result.end());
		return result;
	}
	return result;
} 
//打印哈夫曼树
void PrintHT()
{
	cout << "下标\t" << "数据\t" << "权重\t" << "双亲\t" << "左孩子\t" << "右孩子" << endl;
	for (int i = 0; i<n*2-1; i++)
	{
		cout << i << "\t" << HTree[i].data << "\t" << HTree[i].weight << "\t" 
		<< HTree[i].parent << "\t" << HTree[i].lchild << "\t" << HTree[i].rchild << endl;
	}
}
void PrintHFC(){
	cout<<"数据\t"<<"哈夫曼编码\t"<<endl;
	for(int i=0;i<n;i++){
		cout<<HTree[i].data<<"\t"<<HFCode[i].str<<"\t"<<endl;
	} 
}
int main(){
	Creat();
	PrintHT();
	DealHFCodeString();
	PrintHFC();
	cout<<"此时的解码之后的是"<<HFDecode()<<endl;; 
	while(1){
		cout<<"是否继续解码 是的话 请输入1  否则请输入0"<<endl;
		int flag; cin>>flag;
		if(1==flag){
			cout<<"此时的解码之后的是"<<HFDecode()<<endl;
		}
		else{
			break;
		}
	}
	return 0;	
}

五、求树的权值

其实思想倒是简单 也就是所以叶子结点路径长度乘上权重 叶子结点好找 也就是数组前n个就是叶子结点 路径长度叶子结点往上找到根结点就可以了,下面写出代码

//计算树的权重  
int HFWeight(){
	int sum=0;
	for(int i=0;i<n;i++){//遍历前n个叶子结点求所有叶子结点带权路径长度的和
		int Cur=i;int Deep=0;
		while(HTree[Cur].parent!=-1){ 
			Deep++;
			Cur=HTree[Cur].parent;
		} 
		sum+=HTree[i].weight*Deep;
	} 
	return sum; 
}

请添加图片描述

六、整体代码

#include<bits/stdc++.h>
#define ElemType int
#define Max 100;
using namespace std;
typedef struct HTNode{
	char data;
	double weight;
	int parent=-1;
	int lchild=-1;
	int rchild=-1;
}HTNode;
typedef struct HFNode{
	char data;
	string str;
}HFNode;
int n;
HTNode HTree[100];
HFNode HFCode[100]; 
void Creat(){//创建一个哈夫曼树
	cout<<"请输入你要填入字符的个数"<<endl;vector<int> Vec; 
	cin>>n;
	cout<<"请输入字符以及对应的权值"<<endl;
	for(int i=0;i<n;i++){
		cin>>HTree[i].data>>HTree[i].weight; 
		HFCode[i].data=HTree[i].data;
		Vec.push_back(HTree[i].weight);
	}
	//上面几句完成了初始化  接下来进行造树(填表)
		for(int i=n;i<2*n-1;i++){ //将HTree数组中的剩下的填充完即可结束 
			if(Vec.size()==1) break;
			sort(Vec.begin(),Vec.end());
			//遍历来寻找最小值以及次小值在HTree中的坐标  但是要注意此时是给这两个结点找父亲 所以他们不能已经有了父亲 
			for(int j=0;j<i;j++){
				if(Vec[0]==HTree[j].weight&&HTree[j].parent==-1){//找最小值的父亲 
					HTree[j].parent=i;
					HTree[i].lchild=j;	
					break;//是为了避免重复值的问题 
				}
			} 
			for(int j=0;j<i;j++){
				if(Vec[1]==HTree[j].weight&&HTree[j].parent==-1){
					HTree[j].parent=i;
					HTree[i].rchild=j;
					break;
				}
			} 
			HTree[i].weight=Vec[0]+Vec[1];
			//成功一次之后 需要删除两个 添加一个
			Vec.erase(Vec.begin(),Vec.begin()+2);
			Vec.push_back(HTree[i].weight); 
		}  
}
//哈夫曼编码 
void DealHFCodeString(){
	for(int i=0;i<n;i++){//对n个结点进行string的赋值 
		int Cur=i;//指向此时的第一个叶子节点 
		string Str;
		while(HTree[Cur].parent!=-1){
			if(HTree[HTree[Cur].parent].lchild==Cur){
				Str+=to_string(0);
			}
			else{
				Str+=to_string(1);
			}
			Cur=HTree[Cur].parent;
		} 
		reverse(Str.begin(),Str.end());
		HFCode[i].str=Str;
	}
}
//哈夫曼解码
string HFDecode(){
	string Str;string result;
	cout<<"请输入一段01 构成的字符串"<<endl;
	cin>>Str; int Cur=n*2-2;
	//我们知道树根是放在数组的最后一个的位置上
	while(!Str.empty()){//字符串中为空的时候 也是要退出的 
		if(Str.front()=='1'){//向右深入
			cout<<"向右深入"<<endl; 
			Cur=HTree[Cur].rchild;
			Str.erase(Str.begin(),Str.begin()+1);
		}
		else{//向左深入 
			cout<<"向左深入"<<endl; 
			Cur=HTree[Cur].lchild;
			Str.erase(Str.begin(),Str.begin()+1);
		} 
		if(HTree[Cur].lchild==-1){//说明此时是叶子结点 将此时对应的结果放在result中 
			cout<<"将"<<HTree[Cur].data<<"放入结果集合中"<<endl; 
			result+=HTree[Cur].data;
			Cur=n*2-2;//到达叶子节点重新指向根结点 
		}
	} 
	if(HTree[Cur].parent!=-1){//遍历完成的时候若是Cur指向的不是叶子结点 输入有问题
		cout<<"此时你输入的有问题"<<endl;
		result.erase(result.begin(),result.end());
		return result;
	}
	return result;
} 
//打印哈夫曼树
void PrintHT()
{
	cout << "下标\t" << "数据\t" << "权重\t" << "双亲\t" << "左孩子\t" << "右孩子" << endl;
	for (int i = 0; i<n*2-1; i++)
	{
		cout << i << "\t" << HTree[i].data << "\t" << HTree[i].weight << "\t" 
		<< HTree[i].parent << "\t" << HTree[i].lchild << "\t" << HTree[i].rchild << endl;
	}
}
//打印字母对应的哈夫曼编码 
void PrintHFC(){
	cout<<"数据\t"<<"哈夫曼编码\t"<<endl;
	for(int i=0;i<n;i++){
		cout<<HTree[i].data<<"\t"<<HFCode[i].str<<"\t"<<endl;
	} 
}
//计算树的权重  
int HFWeight(){
	int sum=0;
	for(int i=0;i<n;i++){//遍历前n个叶子结点求所有叶子结点带权路径长度的和
		int Cur=i;int Deep=0;
		while(HTree[Cur].parent!=-1){ 
			Deep++;
			Cur=HTree[Cur].parent;
		} 
		sum+=HTree[i].weight*Deep;
	} 
	return sum; 
}
int main(){
	Creat();
	PrintHT();
	DealHFCodeString();
	PrintHFC();
	cout<<"此时树的权重是"<<HFWeight()<<endl; 
	cout<<"此时的解码之后的是"<<HFDecode()<<endl;
	while(1){
		cout<<"是否继续解码 是的话 请输入1  否则请输入0"<<endl;
		int flag; cin>>flag;
		if(1==flag){
			cout<<"此时的解码之后的是"<<HFDecode()<<endl;
		}
		else{
			break;
		}
	}
	return 0;	
}

总结

有些时候是需要自己来写一下的 建议读者尝试一下 理解了其实也不难 大家加油!感觉不错点个赞呗

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值