提示:昼短苦夜长,何不秉烛游!
再此声明因为构造过程中会有重复值的出现 导致构造的树 并不一定一样 也就导致解码 编码的不同
一、哈夫曼解析
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;
}
总结
有些时候是需要自己来写一下的 建议读者尝试一下 理解了其实也不难 大家加油!感觉不错点个赞呗