哈夫曼树
定义:哈夫曼树又称最优树,是一类带权路径最短的树。
结点的带权路径:结点所代表的数乘以从根到改结点所经过的路(即改结点所在的深度-1)
树的带权路径(WPL):就是指所有结点带权路径和的最小值
那到底什么叫WPL呢?
举个栗子:如图所示的三颗二叉树,都含有四个叶子结点a,b,c,d,权值分别为7,5,2,4;那么它们的WPL 分别为多少呢?
1 2
3
很明显 第三颗树的WPL 的最小 也就是我们所求的哈夫曼树
那么哈夫曼树到底该怎么画呢 ?
这里为了方便起见,我们先直接把权值代表该叶子结点 给你一组权值 2,4,5,3,该怎么画出哈夫曼树呢?
第一步:这四个叶子初始时呢 是这个样子滴 (先排个序
第二步: 我们找出权值最小的两个结点2和3,然后将他们合并
重复第二步: 如果有重复 随便选(所以最后的树可能不一样但是WPL相等) 那这里我们选第二个5和4 合并
继续重复 显然 现在没得选了 合并
这样 一颗哈夫曼树就创建成功了 WPL= 2*2+3*2+4*2+5*2=28
当然 如果在第二步合并的是第一个5和4 那么哈夫曼树为
WPL =2*3+3*3+4*2+5*1=28
画完哈夫曼树之后 有没有发现什么规律?
1)在哈夫曼树中,权值越大的结点距离根节点就越近。
2)哈夫曼树是一颗只有度为1和度为2的二叉树,叶子结点为n个,那么度为2的结点有n-1个,整棵树的结点就有2n-1个。
那么 前面学习了如何画出哈夫曼树,但是如何用代码实现呢?
第一步: 考虑哈夫曼树的结点是什么类型的呢?
由哈夫曼树的特点我们知道结点数一共有2n-1个那么我们可以用一维数组存储这些结点,而对于每一个结点来说,分为四部分,分别存放权值,双亲,左孩子和右孩子;
typedef struct{
int weight;
int parent,lchild,rchild;
}htnode,*nuffmantree;
第二步:创建哈夫曼树
(1) 初始化
动态申请2n-1个空间,循环2n-1次将双亲,左孩子,右孩子置为-1;循环n次,将权值赋给结点的weight;
(2)选取与合并
选出还没有双亲的两个权值最小的结点 ,将它们合并
(3)删除与加入
将上一步选出了两个结点的双亲置更新,新结点的左右孩子更新
(4)重复(2)和(3)n次
代码实现
//构造哈夫曼树
void createnuffmantree(nuffmantree &HT,int w[],int n){
//初始化
HT=new htnode[2*n-1]; //动态申请2n-1个空间
for(int i=0;i<2*n-1;i++){//双亲 ,左右孩子置-1
HT[i].parent=HT[i].lchild=HT[i].rchild=-1;
}
for(int i=0;i<n;i++){//初始化前n个的权值
HT[i].weight=w[i];
}
//选取与合并,删除与加入
for(int k=n;k<2*n-1;k++){
int s1,s2;
Select(HT,k,s1,s2);//选取两个权值最小值的位置
HT[s1].parent=k; //s1位置对应双亲为i
HT[s2].parent=k; //s2位置对应双亲为i
HT[k].lchild=s1; //更新左孩子权值
HT[k].rchild=s2; //更新右孩子权值
HT[k].weight=HT[s1].weight+HT[s2].weight; //更新自己的权值
}
}
Select函数 遍历0~k-1的结点 找出还没有双亲的权值最小的两个结点的下标s1,s2
//Select 函数 找出没有双亲结点的两个位置
void Select(nuffmantree HT,int k,int &s1,int &s2){
int sw1=1e9,sw2=1e9;//初始化两个最大权值
for(int i=0;i<k-1;i++){
if(HT[i].parent==-1&&HT[i].weight<sw1){//没有双亲且对应下标权值小于sw1
s1=i;//确定下标
sw1=min(sw1,HT[i].weight);//更新sw1
}
}
for(int i=0;i<k;i++){
if(HT[i].parent==-1&&HT[i].weight<sw2&&HT[i].weight>sw1){//没有双亲且对应下标权值小于sw2大于sw1
s2=i;//确定下标
sw2=min(sw2,HT[i].weight);//更新sw2
}
}
}
再来个栗子 权值 2 4 5 3
初始化 创建后
哈夫曼树也创建出来了,那么下一步就是编码了
本来以为编码挺难的 其实写出来 也不难嘛~
既然在上一步我们已经画好了哈夫曼树,现在只要对哈夫曼树加一点点东西就好
我们把左枝置0,右枝置1(当然也可以倒过来 随心所欲~)
对于上个例子 我们把 2 4 5 3 分别当做字母 A B C D的权值
结果如图:
这样就得到了 A B C D 的编码 A:00 B:10 C:11 D:01
完整代码实现:
#include<iostream>
#include<cstring>
using namespace std;
typedef struct{
int weight;
int parent,lchild,rchild;
}htnode,*nuffmantree;
int w[101],n;
//Select 函数 找出没有双亲结点的两个位置
void Select(nuffmantree HT,int k,int &s1,int &s2){
int sw1=1e9,sw2=1e9;//初始化两个最大权值
for(int i=0;i<k-1;i++){
if(HT[i].parent==-1&&HT[i].weight<sw1){//没有双亲且对应下标权值小于sw1
s1=i;//确定下标
sw1=min(sw1,HT[i].weight);//更新sw1
}
}
for(int i=0;i<k;i++){
if(HT[i].parent==-1&&HT[i].weight<sw2&&HT[i].weight>sw1){//没有双亲且对应下标权值小于sw2大于sw1
s2=i;//确定下标
sw2=min(sw2,HT[i].weight);//更新sw2
}
}
}
//构造哈夫曼树
void createnuffmantree(nuffmantree &HT,int w[],int n){
//初始化
HT=new htnode[2*n-1]; //动态申请2n-1个空间
for(int i=0;i<2*n-1;i++){//双亲 ,左右孩子置-1
HT[i].parent=HT[i].lchild=HT[i].rchild=-1;
}
for(int i=0;i<n;i++){//初始化前n个的权值
HT[i].weight=w[i];
}
//选取与合并,删除与加入
for(int k=n;k<2*n-1;k++){
int s1,s2;
Select(HT,k,s1,s2);//选取两个权值最小值的位置
HT[s1].parent=k; //s1位置对应双亲为i
HT[s2].parent=k; //s2位置对应双亲为i
HT[k].lchild=s1; //更新左孩子权值
HT[k].rchild=s2; //更新右孩子权值
HT[k].weight=HT[s1].weight+HT[s2].weight; //更新自己的权值
}
}
//哈夫曼编码
void creathuffmancode(nuffmantree HT,char ** &HC,int n){
HC=new char *[n]; //可以将HC理解为二维的char[][]数组
char *cd=new char[n];//cd 为一维的char[]数组
cd[n-1]='\0';
for(int i=0;i<n;i++){
int start=n-1;
int c=i;//定位第几个字符
int f=HT[i].parent;//第i个位置的双亲
while(f!=-1){//由叶子向双亲遍历
start--;
if(HT[f].lchild==c) cd[start]='0';//左孩子为0
else cd[start]='1';//右孩子为1
c=f;f=HT[f].parent;//c记录当前位置,更新f
}
HC[i]=new char [n-start];
strcpy(HC[i],&cd[start]);//将cd[]内容复制给HC[i]
}
delete cd;
}
int main(){
nuffmantree HT;
char **HC;
cout<<"请输入字符个数:";
cin>>n;
char a[256];
cout<<"请分别输入字符及对应的权值:"<<endl;
for(int i=0;i<n;i++)
cin>>a[i]>>w[i];
createnuffmantree(HT,w,n);//建树
creathuffmancode(HT,HC,n);//编码
for(int i=0;i<n;i++){//输出
cout<<a[i]<<": ";
for(int j=0;j<strlen(HC[i]);j++)
cout<<HC[i][j];
cout<<endl;
}
return 0;
}