36. 数据结构笔记之三十六赫夫曼树
“假如生活欺骗了你,不要忧郁,也不要愤慨!不顺心的时候暂且容忍:相信吧,快乐的日子就会到来。 --普希金”
我们来学习赫夫曼树,当然也是哈夫曼树,音译不同,大伙不用太较劲。
什么是赫夫曼呢?为什么叫赫夫曼树呢?
1. 简介赫夫曼在上世纪五十年代初就提出这种编码时,根据字符出现的概率来构造平均长度最短的编码。它是一种变长的编码。在编码中,若各码字长度严格按照码字所对应符号出现概率的大小的逆序排列,则编码的平均长度是最小的。(注:码字即为符号经赫夫曼编码后得到的编码,其长度是因符号出现的概率而不同,所以说赫夫曼编码是变长的编码。)而且赫夫曼编码是按照子树到父亲,而其读码则是完全相反的。
有点拗口,咱们就来看下基本概念。(代码均来自网络,由蛤蟆实测可用)
2. 基本概念a、路径和路径长度
若在一棵树中存在着一个结点序列 k1,k2,……,kj,使得 ki是ki+1 的双亲(1<=i<j),则称此结点序列是从 k1 到 kj 的路径。
从 k1 到 kj 所经过的分支数称为这两点之间的路径长度,它等于路径上的结点数减1.
b、结点的权和带权路径长度
在许多应用中,常常将树中的结点赋予一个有着某种意义的实数,我们称此实数为该结点的权,结点的带权路径长度规定为从树根结点到该结点之间的路径长度与该结点上权的乘积。
c、树的带权路径长度
树的带权路径长度定义为树中所有叶子结点的带权路径长度之和,公式如下图 1:
其中,n表示叶子结点的数目,wi 和 li 分别表示叶子结点 ki 的权值和树根结点到 ki 之间的路径长度。
如下图2
中树的带权路径长度 WPL= 9 x 2 + 12 x 2 + 15 x 2 + 6 x 3 + 3 x 4 + 5 x 4 = 122
那什么是赫夫曼树呢?
3. 赫夫曼树
给定n个权值作为n的叶子结点,构造一棵二叉树,若带权路径长度达到最小,称这样的二叉树为最优二叉树,也称为赫夫曼树(Huffmantree)。赫夫曼树是带权路径长度最短的树,权值较大的结点离根较近。
3.1 构造赫夫曼树
假设有n个权值,则构造出的赫夫曼树有n个叶子结点。 n个权值分别设为 w1、w2、…、wn,则赫夫曼树的构造规则为:
(1) 将w1、w2、…,wn看成是有n 棵树的森林(每棵树仅有一个结点);
(2) 在森林中选出两个根结点的权值最小的树合并,作为一棵新树的左、右子树,且新树的根结点权值为其左、右子树根结点权值之和;
(3)从森林中删除选取的两棵树,并将新树加入森林;
(4)重复(2)、(3)步,直到森林中只剩一棵树为止,该树即为所求得的赫夫曼树。
如:对下图3中的六个带权叶子结点来构造一棵赫夫曼树,步骤如下:
注意:为了使得到的赫夫曼树的结构尽量唯一,通常规定生成的赫夫曼树中每个结点的左子树根结点的权小于等于右子树根结点的权。
3.2 赫夫曼编码
在电报通信中,电文是以二进制的0、1序列传送的,每个字符对应一个二进制编码,为了缩短电文的总长度,采用不等长编码方式,构造赫夫曼树,
将每个字符的出现频率作为字符结点的权值赋予叶子结点,每个分支结点的左右分支分别用0和1编码,从树根结点到每个叶子结点的路径上
所经分支的0、1编码序列等于该叶子结点的二进制编码。如上文所示的赫夫曼编码如下图4:
a 的编码为:00
b 的编码为:01
c 的编码为:100
d 的编码为:1010
e 的编码为:1011
f 的编码为:11
4. 赫夫曼树代码4.1 定义结构体
节点结构体,包含一个整型和两个子叶的指针。
structBTreeNode
{
ElemTypedata;
structBTreeNode*left;
structBTreeNode*right;
};
4.2 Main
输入叶子的节点数,如果n<=1则重新输入,如果>1则继续。
为n个节点分配存储空间,然后为每个子叶节点分配权值。
调用createHuffman函数创建赫夫曼树,然后输出赫夫曼树,输出带权路径长度,最后进行赫夫曼编码。
如下图5
4.3 CreateHuffman
输入参数为记录权值的数组和数组的个数。
根据数组个数创建n个存储空间。
然后数组b中的每个元素指向一个权值。
K1,k2表示最小和次小结点的下标。
为K1和K2的节点创建一个新的节点,其左节点为k1,右节点为k2.
通过n-1循环后,刚好剩下一个根节点。
4.4 PrintBTree_int
输入参数为根节点,然后输出根节点的值,如果左右节点都为NULL,则结束。
否则输出左子树(也是先输出左子树的根节点,在输入左子树的左子树),然后输出右子树。
4.5 WeightPathLength
输入参数是根节点和路径,路径初始为0,递归调用时会发生变化。如果根节点为NULL则返回,如果左右子结点都为NULL,则输出权值。
否则递归调用。
4.6 HuffManCoding
赫夫曼编码函数,输入参数为根节点和len整型。Len用来递归使用。
用数组来保存每个叶子的编码。
如果根节点为NULL则退出,否则继续。
如果函数退出,则进行判断左子节点和右子节点是否为NULL,都为NULL 说明是叶子节点,就输出该节点的编码。如果不是叶子节点,则则递归调用。
4.7 源码
#include<stdio.h>
#include<stdlib.h>
typedefintElemType;
structBTreeNode
{
ElemTypedata;
structBTreeNode*left;
structBTreeNode*right;
};
//1、输出二叉树,可在前序遍历的基础上修改。采用广义表格式,元素类型为int
void PrintBTree_int(structBTreeNode*BT)
{
if (BT!=NULL)
{
printf("%d",BT->data);//输出根结点的值
if (BT->left!=NULL ||BT->right!=NULL)
{
printf("(");
PrintBTree_int(BT->left);//输出左子树
if (BT->right!=NULL)
printf(",");
PrintBTree_int(BT->right);//输出右子树
printf(")");
}
}
}
//2、根据数组 a中 n 个权值建立一棵赫夫曼树,返回树根指针
structBTreeNode*CreateHuffman(ElemTypea[],intn)
{
inti, j;
structBTreeNode**b, *q;
b =malloc(n*sizeof(structBTreeNode));
for(i = 0; i <n; i++)//初始化b指针数组,使每个指针元素指向a数组中对应的元素结点
{
b[i] =malloc(sizeof(structBTreeNode));
b[i]->data = a[i];
b[i]->left = b[i]->right = NULL;
}
for(i = 1; i <n; i++)//进行 n-1次循环建立赫夫曼树
{
//k1表示森林中具有最小权值的树根结点的下标,k2为次最小的下标
intk1 = -1, k2;
for(j = 0; j <n; j++)//让k1初始指向森林中第一棵树,k2指向第二棵
{
if(b[j] !=NULL && k1 == -1)
{
k1 = j;
continue;
}
if(b[j] !=NULL)
{
k2 = j;
break;
}
}
for(j = k2; j <n; j++)//从当前森林中求出最小权值树和次最小
{
if(b[j] !=NULL)
{
if (b[j]->data <b[k1]->data)
{
k2 = k1;
k1 = j;
}
elseif(b[j]->data < b[k2]->data)
k2 = j;
}
}
//由最小权值树和次最小权值树建立一棵新树,q指向树根结点
q =malloc(sizeof(structBTreeNode));
q->data = b[k1]->data + b[k2]->data;
q->left = b[k1];
q->right = b[k2];
b[k1]= q;//将指向新树的指针赋给b指针数组中k1位置
b[k2]= NULL;//k2位置为空
}
free(b); //删除动态建立的数组b
returnq;//返回整个赫夫曼树的树根指针
}
//3、求赫夫曼树的带权路径长度
ElemType WeightPathLength(structBTreeNode*FBT,intlen)//len初始为0
{
if (FBT==NULL)//空树返回0
return0;
else
{
if (FBT->left==NULL &&FBT->right==NULL)//访问到叶子结点
returnFBT->data*len;
else//访问到非叶子结点,进行递归调用,返回左右子树的带权路径长度之和,len递增
returnWeightPathLength(FBT->left,len+1)+WeightPathLength(FBT->right,len+1);
}
}
//4、赫夫曼编码(可以根据赫夫曼树带权路径长度的算法基础上进行修改)
void HuffManCoding(structBTreeNode*FBT,intlen)//len初始值为0
{
staticinta[10];//定义静态数组a,保存每个叶子的编码,数组长度至少是树深度减一
if (FBT!=NULL)//访问到叶子结点时输出其保存在数组a中的0和1序列编码
{
if (FBT->left==NULL &&FBT->right==NULL)
{
inti;
printf("结点权值为%d的编码:",FBT->data);
for(i = 0; i <len; i++)
printf("%d",a[i]);
printf("\n");
}
else//访问到非叶子结点时分别向左右子树递归调用,并把分支上的0、1编码保存到数组a
{ //的对应元素中,向下深入一层时len值增1
a[len]= 0;
HuffManCoding(FBT->left,len +1);
a[len]= 1;
HuffManCoding(FBT->right,len +1);
}
}
}
//主函数
voidmain()
{
int n,i;
ElemType*a;
structBTreeNode*fbt;
printf("从键盘输入待构造的赫夫曼树中带权叶子结点数n:");
while(1)
{
scanf("%d",&n);
if(n > 1)
break;
else
printf("重输n值:");
}
a = malloc(n*sizeof(ElemType));
printf("从键盘输入%d个整数作为权值:",n);
for (i = 0; i< n; i++)
scanf("%d", &a[i]);
fbt = CreateHuffman(a, n);
printf("广义表形式的赫夫曼树:");
PrintBTree_int(fbt);
printf("\n");
printf("赫夫曼树的带权路径长度:");
printf("%d\n",WeightPathLength(fbt, 0));
printf("树中每个叶子结点的赫夫曼编码:\n");
HuffManCoding(fbt, 0);
}