定义 : 给定N个权值作为N个叶子结点,构造一棵二叉树,若该树的带权路径长度达到最小,称这样的二叉树为最优二叉树,也称为哈夫曼树(Huffman Tree)。哈夫曼树是带权路径长度最短的树,权值较大的结点离根较近。
已知有4个叶子结点,他们的值分别是a=1, b=2, c=4, d=10;
用这4个叶子结点构建成一个二叉树
这个27称为带权路径长度,即所有叶结点的权值(就是a,b,c,d的值)乘上其到根结点的路径长度(即经过了几条线)
当这个带权路径长度最小的时候,我们就称他为哈夫曼树,也称最优二叉树
假设有n个权值,则构造出的哈夫曼树有n个叶子结点。 n个权值分别设为 w1、w2、…、wn,则哈夫曼树的构造规则为:
(1) 将w1、w2、…,wn看成是有n 棵树的森林(每棵树仅有一个结点);
(2) 在森林中选出两个根结点的权值最小的树合并,作为一棵新树的左、右子树,且新树的根结点权值为其左、右子树根结点权值之和;
(3)从森林中删除选取的两棵树,并将新树加入森林;
(4)重复(2)、(3)步,直到森林中只剩一棵树为止,该树即为所求得的哈夫曼树。
这里有两个权值为8的结点,我们只需要任意取一个权值为8的结点就行;
从这里也可以看出,哈夫曼树是不唯一的,但是他们带权路径长度都相同;
以下面的树为例
首先定义下哈夫曼树的存储结构;
这里采用的是一个大小为2n-1的向量来存储哈夫曼树中的结点
“假设有n个叶子结点,哈夫曼树的总结点数为2n-1”
typedef struct
{ int weght; //结点的权值,为了方便直接用int
int parent,lchild,rchild; //左右孩子和双亲
}HTNode,*HuffmanTree;
createHuffmanTree函数
//HT为树,n为叶子结点数,w[]叶子的权值
void CreateHuffmanTree (HuffmanTree HT,int n,int w[]){
if(n<=1) // 如果结点小于等于1,直接退出
return;
m=2*n-1; //总共有2*n-1个结点
HT=new HTNode[m+1]; //为了方便,不用0号单元,HT[m]表示根结点,下表从1开始
/*给所有结点的左,右孩子以及双亲初始化为0;*/
for(i=1;i<=m;++i)
{
HT[i].lchild=0;
HT[i].rchild=0;
HT[i].parent=0;
}
/*给前 n 个结点设置权重*/
for(i=1;i<=n;i++)
{
HT[i].weight = w[i];
}
/*给第 n 个结点之后的结点赋值,以及设置左右孩子和双亲*/
for( i=n+1;i<=m;++i){
/*
调用Select函数
在前 i-1 个没有双亲的结点中返回权重最小的结点序号s1,s2
*/
Select(HT,i-1, s1, s2);
/*
将第 s1,s2 号结点的双亲设置为第 i 个结点
同时意味着下次调用Select函数将忽略s1,s2
*/
HT[s1].parent=i;
HT[s2] .parent=i;
/*s1,s2分别作为i的左右孩子*/
HT[i].lchild=s1;
HT[i].rchild=s2;
/*i 的权值为左右孩子权值之和*/
HT[i].weight=HT[s1].weight + HT[s2] .weight;
}
}
实现Select函数
void Select(HuffmanTree HT,int n,int &index1,int &index2){
if(n<=1)
return;
for(int k=1;k<=n;k++){ //给index1赋初值
if(HT[k].parent == 0){
index1 = k++;
break;
}
}
for(;k<=n;k++){
if(HT[k].parent == 0){ //给index2赋初值
index2 = k++;
break;
}
}
for(k=1;k<=n;k++){ //找到最小的权值,赋给index1
if(HT[k].parent == 0){
if(HT[index1].weight > HT[k].weight){
index2 =index1;
index1 =k;
}
}
}
HT[index1].parent = -1; //将最小的权值结点双亲改为-1,防止被index2选取
for(int i=1;i<=n;i++){ //找到第二小的权值,赋给index2
if(HT[i].parent == 0){
if(HT[index2].weight > HT[i].weight){
index2 =i;
}
}
}
}
在远程通讯中,要将字符转换成二进制的字符串来传送
假如有一串字符ABACCDAAAC
常规的编码是 A = 00; B = 01 ;C = 10; D = 11;
00 01 00 10 10 11 00 00 00 00 01;
而如果采用哈夫曼编码的方式:A = 0;B = 110 ; C =10; D =111 ;
0 110 0 10 10 111 0 0 0 0 10;
这个差值会在字符变多,文本变长的情况越拉越大,而使用不等长编码的关键就在于采用 前缀编码,否者会使数据错乱,前缀编码可以采用二叉树设计;
哈夫曼编码就是使出现次数较多的字符采用尽可能短的编码方式;
哈夫曼编码是可变字长编码(VLC)的一种。Huffman于1952年提出一种编码方法,该方法完全依据字符出现概率来构造异字头的平均长度最短的码字,有时称之为最佳编码,一般就叫做Huffman编码(有时也称为霍夫曼编码)。
如图所示 先将哈夫曼树画出来,左孩子为0,右孩子为1
A的编码就是: 000 ;
B为: 001;
C为: 01;
D为: 10;
E为: 11;
这里我们仍然用创建哈夫曼树时的例子
首先创建两个数组,一个存放叶子结点的权值,
int *w[5]={0,70,50,20,40};
不用第 0 号位置,设为0;
一个放叶子是其双亲的左孩子还是右孩子,左孩子为零,右孩子为1;
char * cd;
//HT为树,n为叶子结点数
void codeArray(HuffmanTree HT,int n){
cd = (char)malloc((2*n)*sizeof(char));
cd[2n-1] = '#';//将最后一个赋值为"#",因为没有双亲结点
for(int j = 1;j<=2*n;j++){
if(HT[j].parent != 0) {
if(HT[HT[j].parent].lchild == j)//判断第 j 个结点是否为双亲结点的左孩子
cd[j] = '0';
}else{
cd[j] = '1';
}
}
}
完成代码即可创造如下的数组
创建一个指向字符指针的指针,该数组存放每个叶子结点的哈夫曼编码
Typedef char **Huffmancode;
void createHuffmanCode(HuffmanTree HT,int n, HuffmanCode &HC){
HC = (HuffmanCode)malloc((n+1)*sizeof(char*));//分配数组结点空间,依旧不用0号位
int count = 1; //用来计算每个叶子结点到根结点的路径长度
int code = 0; //当前节点的双亲结点位置
for(int i=1;i<=n;i++){
count = 1;
for(int f=HT[i].parent;f != 0;f=HT[f].parent){
count++;
}
HC[i] = (char*)malloc((count)*sizeof(char));
for(code=i;count>1;count--){
HC[i][count-1]= cd[code]; //将cd数组上的'0','1'字符倒叙放在HC上
code = HT[code].parent; //code赋值为当前节点的双亲结点位置
}
}
}
下面给出测试的主程序
/*测试函数*/
int main(){
HuffmanTree HT; //创建哈夫曼树
HuffmanCode HC; //创建哈夫曼编码数组
int n=4;
int w[5]={0,70,50,20,40}; //测试的叶子结点权值
CreateHuffmanTree (HT,n,w);
codeArray(HT,n);
createHuffmanCode(HT,n,HC);
for(int i=1;i<=n;i++){ //顺序输出a,b,c,d的哈夫曼编码
cout<<(char)('a'+i-1)<<"的哈夫曼编码:";
int j =1;
while(HC[i][j] == '0' || HC[i][j] == '1' ){
cout<<HC[i][j];
j++;
}
cout<<endl;
}
return 0;
}