转载请注明出处:http://blog.csdn.net/ns_code/article/details/19174553
Huffman Tree简介
赫夫曼树(Huffman Tree),又称最优二叉树,是一类带权路径长度最短的树。假设有n个权值{w1,w2,...,wn},如果构造一棵有n个叶子节点的二叉树,而这n个叶子节点的权值是{w1,w2,...,wn},则所构造出的带权路径长度最小的二叉树就被称为赫夫曼树。
这里补充下树的带权路径长度的概念。树的带权路径长度指树中所有叶子节点到根节点的路径长度与该叶子节点权值的乘积之和,如果在一棵二叉树中共有n个叶子节点,用Wi表示第i个叶子节点的权值,Li表示第i个也叶子节点到根节点的路径长度,则该二叉树的带权路径长度 WPL=W1*L1 + W2*L2 + ... Wn*Ln。
根据节点的个数以及权值的不同,赫夫曼树的形状也各不相同,赫夫曼树具有如下特性:
- 对于同一组权值,所能得到的赫夫曼树不一定是唯一的。
- 赫夫曼树的左右子树可以互换,因为这并不影响树的带权路径长度。
- 带权值的节点都是叶子节点,不带权值的节点都是某棵子二叉树的根节点。
- 权值越大的节点越靠近赫夫曼树的根节点,权值越小的节点越远离赫夫曼树的根节点。
- 赫夫曼树中只有叶子节点和度为2的节点,没有度为1的节点。
- 一棵有n个叶子节点的赫夫曼树共有2n-1个节点。
赫夫曼树的构建步骤如下:
1、将给定的n个权值看做n棵只有根节点(无左右孩子)的二叉树,组成一个集合HT,每棵树的权值为该节点的权值。
2、从集合HT中选出2棵权值最小的二叉树,组成一棵新的二叉树,其权值为这2棵二叉树的权值之和。
3、将步骤2中选出的2棵二叉树从集合HT中删去,同时将步骤2中新得到的二叉树加入到集合HT中。
4、重复步骤2和步骤3,直到集合HT中只含一棵树,这棵树便是赫夫曼树。
假如给定如下5个权值:
则按照以上步骤,可以构造出如下面左图所示的赫夫曼树,当然也可能构造出如下面右图所示的赫夫曼树,这并不是唯一的。
赫夫曼树的应用十分广泛,比如众所周知的在通信电文中的应用。在等传送电文时,我们希望电文的总长尽可能短,因此可以对每个字符设计长度不等的编码,让电文中出现较多的字符采用尽可能短的编码。为了保证在译码时不出现歧义,我们可以采取如下图所示的编码方式:
即左分支编码为字符0,右分支编码为字符1,将从根节点到叶子节点的路径上分支字符组成的字符串作为叶子节点字符的编码,这便是赫夫曼编码。我们根据上面左图可以得到各叶子节点的赫夫曼编码如下:
权值为5的也自己节点的赫夫曼编码为:11
权值为4的也自己节点的赫夫曼编码为:10
权值为3的也自己节点的赫夫曼编码为:00
权值为2的也自己节点的赫夫曼编码为:011
权值为1的也自己节点的赫夫曼编码为:010
而对于上面右图,则可以得到各叶子节点的赫夫曼编码如下:
权值为5的也自己节点的赫夫曼编码为:00
权值为4的也自己节点的赫夫曼编码为:01
权值为3的也自己节点的赫夫曼编码为:10
权值为2的也自己节点的赫夫曼编码为:110
权值为1的也自己节点的赫夫曼编码为:111
Huffman编码的C实现
由于赫夫曼树中没有度为1的节点,则一棵具有n个叶子节点的的赫夫曼树共有2n-1个节点(最后一条特性),因此可以将这些节点存储在大小为2n-1的一维数组中。我们可以用以下数据结构来表示赫夫曼树和赫夫曼编码:
typedef struct Node
{
int weight;//权重
int parent;//父结点的序号,为-1的根节点
int lchild,rchild;//左右孩子结点的序号,为-1的是叶子节点
}HTNode,*HuffmanTree;
typedef char **HuffmanCode;//用来存储每个叶子节点的霍夫曼编码
根据赫夫曼树的构建步骤,我们可以写出构建赫夫曼树的代码如下:
void select_minium(HuffmanTree HT,int k,int &min1,int &min2);
int minn(HuffmanTree HT,int k);
/*
根据给定的n个权值构造一棵霍夫曼树,wet中存放n个权值
*/
HuffmanTree creat_HuffmanTree(int *wet,int n)
{
//一棵有n个叶子节点的霍夫曼树共有2*n-1个节点
int total=2*n-1;
HuffmanTree HT=(HuffmanTree)malloc(total*sizeof(HTNode));//申请内存分配
if(!HT)
{
printf("HuffmanTree malloc failed!");
exit(-1);
}
//以下初始化序号全部用-1表示
//这样在编码函数中进行循环判断parent或者lchild或rchild的序号时,
//不会与HT数组的任何一个下标混淆。
//HT[0],HT[1]...HT[n-1]中存放需要编码的n个叶子节点
for(int i=0;i<n;++i)
{
HT[i].parent=-1;
HT[i].lchild=-1;
HT[i].rchild=-1;
HT[i].weight=*wet;
wet++;
}
//HT[n],HT[n+1]...HT[2n-2]中存放的是中间构造出的每颗二叉树的根节点
for(int i=n;i<total;++i)
{
HT[i].parent=-1;
HT[i].lchild=-1;
HT[i].rchild=-1;
HT[i].weight=0;
}
int min1,min2;//用来保存每一轮选出的两个weight最小且parent为0的节点
//每一轮比较后选择出min1和min2构成一棵二叉树,最后构成一棵霍夫曼树
for(int i=n;i<total;++i)
{
select_minium(HT,i,min1,min2);
HT[min1].parent=i;
HT[min2].parent=i;
//霍夫曼树的做右孩子可以反过来,只是所得的编码不同。
HT[i].lchild=min1;
HT[i].rchild=min2;
HT[i].weight=HT[min1].weight+HT[min2].weight;
}
return HT;
}
上述代码中调用到了select_minium()函数,它表示从集合中选出两个最小的二叉树,代码如下:
/*
从HT数组的前k个元素选出weight最小且parent为-1的两个,分别将其序号保存在min1和min2中
*/
void select_minium(HuffmanTree HT,int k,int &min1,int &min2)
{
min1=minn(HT,k);
min2=minn(HT,k);
}
/*
从HT数组的前k个元素中,选出weight最小且parent为-1的元素,并返回元素序号
*/
int minn(HuffmanTree HT,int k)
{
int i=0;
int minnum=0;//用来存放weight最小且parent为-1的元素的序号
int min_weight;//用来存放weight最小且parent为-1的元素的weight值。
//先将第一个parent为-1的元素的weight值赋给min_weight,留作以后比较用
//注意,这里不能按照一般的做法,先直接将HT[0].weight赋给min_weight,
//因为如果如果HT[0].weight的值比较小,那么在第一次构造二叉树时就会被选走,
//而后续的每一轮选择最小权值构造二叉树的比较还是先用HT[0].weight的值来进行判断,
//这样又会再次将其选走,从而产生逻辑上的错误。
while(HT[i].parent!=-1)
++i;
min_weight=HT[i].weight;
minnum=i;
//选出weight最小且parent为-1的元素,并将其序号赋给min
for(;i<k;++i)
{
if(HT[i].weight<min_weight&&HT[i].parent==-1)
{
min_weight=HT[i].weight;
minnum=i;
}
}
//选出weight最小的元素后,将其parent置为1,使得下一次比较时将其排除在外
HT[minnum].parent=1;
return minnum;
}
构建了赫夫曼树,便可以进行赫夫曼编码了,要求赫夫曼编码,就需要遍历出从根节点到叶子节点的路径,下面给出两种遍历赫夫曼树求编码的方法。
//采用从叶子节点到根节点逆向求霍夫曼树HT中n个叶子节点的霍夫曼编码,并保存在HC中
void HuffmanCoding(HuffmanTree HT,HuffmanCode &HC,int n)
{
//用来保存指向每个霍夫曼编码串的指针
HC=(HuffmanCode)malloc(n*sizeof(char *));
if(!HC)
{
printf("HuffmanCode malloc failed!");
exit(-1);
}
//临时空间,用来保存每次求得的霍夫曼编码串
//对于有n个叶子结点的霍夫曼树,各个叶子结点的编码长度最长不超过n-1
//外加一个'\0'结束符,因此分配的数组长度最长为n即可
char *code=(char *)malloc(n*sizeof(char));
if(!code)
{
printf("code malloc failed!");
exit(-1);
}
code[n-1]='\0';//编码结束符
//求每个字符的霍夫曼编码
int i;
for(i=0;i<n;++i)
{
int current=i;//定义当前访问的节点
int father=HT[i].parent;//当前节点的父节点
int start=n-1;//每次编码的位置,初始为编码结束符的位置
//从叶子节点遍历霍夫曼树直到根节点
while(father!=-1)
{
if(HT[father].lchild==current)//如果是左孩子,则编码为0
code[--start]='0';
else
code[--start]='1';
current=father;
father=HT[father].parent;
}
//为第i个字符的编码串分配存储空间
HC[i]=(char *)malloc((n-start)*sizeof(char));
if(!HC[i])
{
printf("HC[i] malloc failed!");
exit(-1);
}
//将编码串从code复制到HC
strcpy(HC[i],code+start);
}
free(code);//释放保存编码串的临时空间
}
该方法与方法1不同,它是根据赫夫曼树的构造来求每个字符的编码的,程序构造的赫夫曼树如上图中的左图所示,那么该方法便是按照3、1、2、4、5的顺序来球每个字符的赫夫曼编码的,但是我们在main函数中将其按照输入的顺序(5、4、3、2、1)打印到了终端。