哈夫曼树
哈夫曼树的定义
哈夫曼树又称为最优二叉树,它是由n个带权值的叶子结点所构成的所有二叉树中带权路径长度最小的二叉树
(树的带权路径长度:树中所有叶子结点的权值乘以相应的树根结点的路径长度之和)
例如:对于权值分别为2, 3, 4, 6的叶子结点,可以构成如下图所示的二叉树
我们可以分别算出这些二叉树的带权路径长度
树a的带权路径长度 : 2×2+3×2+4×2+6×2=30
树b的带权路径长度 : 2×1+3×2+4×3+6×3=38
树c的带权路径长度 : 2×3+3×3+4×2+6×1=29
所以在这3棵二叉树中,树c的带权路径长度最小
哈夫曼树的构造思想
(1)用n棵只有一个根结点的二叉树存放给定的n个权值,它们共同构造成一个森林
(2)在森林中选取两棵树根结点的权值最小的二叉树作为左右子树构造成一棵新的二叉树,新二叉树的根结点的权值为原来两棵树根结点的权值之和
(3)将上面选择的这两棵根结点的权值最小的二叉树从森林中删除,并将新构造的二叉树加入到森林中
(4)重复上面的(2) (3),直到森林中只有一棵二叉树为止,这棵二叉树就是哈夫曼树
假设有一组权值{2, 3, 6, 9},下面我们利用这组权值演示构造哈夫曼树的过程
步骤一:选取两个权值最小的2和3,构造一棵二叉树,然后将2和3删除,并将新建的二叉树的5加入到森林中
步骤二:选取两个权值最小的5和6,构造一棵二叉树,然后将5和6删除,并将新建的二叉树的11加入到森林中
步骤三:选取两个权值最小的9和11,构造一棵二叉树,然后将9和11删除,并将新建的二叉树的20加入到森林中。此时森林中只有一棵二叉树,所以这棵二叉树就是哈夫曼树
它的带权路径长度为:2×3+3×3+6×2+9×1=36
哈夫曼树的构造算法
在构造哈夫曼树时,我们可以设置一个结构数组HT,用来保存哈夫曼树中各结点的信息。由二叉树的性质可知:具有n个叶子结点的哈夫曼树共有2n-1个结点,所以数组HT所需要的存储空间为2n-1,其结构如下:
其中,weight域用来保存结点的权值,lchild域和rchild域分别用来保存该结点的左,右孩子结点在HT数组中的序号,而parent域则用来判断一个结点是否加入到要建立的哈夫曼树中。初始时令parent值为-1,当结点加入到树中,该结点的parent值为其双亲结点在数组HT中的序号
具体操作
构造哈夫曼树时,首先初始化,令2n-1个结点的weight域为0,其它域为-1,然后将n个权值存放到数组HT的前n个分量的weight域中(如下图所示)。
接着不断将未加入哈夫曼树(即parent域等于-1)的两个权值最小的子树合并成一个较大的子树,每次构成的新子树的根结点按顺序放到HT数组的前n个分量的后面,并修改结点相应域的值。
假设此时数组下标为1和3的权值x2和x4最小,则将它们所构成的新子树的根结点放在HT[n]上,然后将HT[1]和HT[3]的parent域改为新子树的根结点的下标,将HT[n]中的weight域改为x2+x4,将HT[n]中的lchild域改为x2,将HT[n]中的rchild域改为x4
假设此时数组下标为2和n-1的权值x3和n最小,则将它们所构成的新子树的根结点放在HT[n+1]上,然后将HT[2]和HT[n-1]的parent域改为新子树的根结点的下标,将HT[n+1]中的weight域改为x3+xn,将HT[n]中的lchild域改为x3,将HT[n]中的rchild域改为xn
最终显示哈夫曼树时,只需找到lchild域不为-1的所有结点,然后依次输出它们的weight域和lchild域,weight域和rchild域即可
哈夫曼树的源代码
# include <stdio.h>
# define MAX 100
typedef struct
{
int weight;
int lchild, rchild, parent;
}HFMT;
//哈夫曼树的初始化
void InitHFMT (HFMT HF[MAX], int n)
{
int i;
//下面的for循环是给每个结点初始化
for (i = 0; i < 2*n-1; i++)
{
HF[i].weight = 0;
HF[i].lchild = -1;
HF[i].rchild = -1;
HF[i].parent = -1;
}
//下面的for循环给叶子结点的weight域赋初值
for (i = 0; i < n; i++)
{
printf ("请输入第%d个结点的权值:",i+1);
scanf ("%d", &HF[i].weight);
getchar ();
//吸收回车键
}
}
//找到权值最小的两个节点
void Min (HFMT HF[MAX], int n, int* p, int* r, int j)
{
int Min1 = 9999;
int Min2 = 9999;
int i;
//下面的for循环是为了找到最小的权值所对应的数组下标
for (i = 0; i < j; i++)
{
if (HF[i].parent == -1 && HF[i].weight < Min1)
//HF[i].parent == -1表示未加入哈夫曼树
{
Min1 = HF[i].weight;
*p = i;
}
}
//下面的for循环是为了找到次最小的权值所对应的数组下标
for (i = 0; i < j; i++)
{
if (HF[i].parent == -1 && HF[i].weight < Min2 && HF[i].weight > Min1)
{
Min2 = HF[i].weight;
*r = i;
}
}
}
//哈夫曼树的创建
void CreateHFMT (HFMT HF[MAX], int n)
{
int p, r;
int i, k;
InitHFMT (HF, n);
for (i = n; i < 2*n -1; i++)
{
Min (HF, n, &p, &r, i);
HF[p].parent = i;
HF[r].parent = i;
HF[i].lchild = HF[p].weight;
HF[i].rchild = HF[r].weight;
HF[i].weight = HF[i].lchild + HF[i].rchild;
}
}
void DisHFMT (HFMT HF[MAX], int n)
{
int i;
printf ("\n哈夫曼树的各边显示:\n");
for (i = 0; i < 2*n-1; i++)
{
if (HF[i].lchild != -1)
{
printf ("(%d, %d), (%d, %d)\n", HF[i].weight, HF[i].lchild, HF[i].weight, HF[i].rchild);
}
}
}
int main (void)
{
int n;
HFMT HF[MAX];
printf ("请输入权值的个数:");
scanf ("%d", &n);
CreateHFMT (HF, n);
DisHFMT (HF, n);
return 0;
}
运行结果
哈夫曼编码
什么是哈夫曼编码
在进行快速远距离的通信时,经常需要将传送的文字转换成由二进制字符0, 1组成的二进制代码,称之为编码
如果在编码是考虑字符出现的频率,让出现频率高的字符采用短的编码,出现频率低的字符采用长的代码,构造一种不等长编码,则电文的代码就可能更短。哈夫曼编码是一种使电文编码总长最短的编码方案
生成哈夫曼树编码的方法
要设计长短不同的编码,首先要做到不同字符的编码不会混淆,即任意一个字符的编码都不是另一个字符编码的前缀(即不是另一个字符编码的开头一部分),满足这个条件的编码就是前缀编码。利用哈夫曼树就可以轻松地设计出这种编码
(1)构造哈夫曼树。让需要编码的字符作为叶子结点,它们在电文中出现的频率作为它们的权值。
(2)在哈夫曼树上求叶子结点的编码。规定哈夫曼树中的左分支代表0,右分支代表1,从根结点到每个叶子结点所经过的路径分支组成0和1的序列就是该结点对应字符的编码
在哈夫曼树编码树中,树的带权路径长度的含义是各个字符的码长与其出现的次数的乘积之和,也就是电文的代码总长。采用哈夫曼树构造的代码是一种能使电文代码总长为最短的不等长代码
例如:设有A,B,C,D四个字符,其出现的频率分别为0.10, 0.15, 0.30, 0.45
则各个字符的编码为:
A:000
B:001
C;01
D:1
哈夫曼编码的源代码
```c
# include <stdio.h>
# define MAX 100
typedef struct
{
int weight;
int lchild, rchild, parent;
}HFMT;
//哈夫曼树的初始化
void InitHFMT (HFMT HF[MAX], int n)
{
int i;
//下面的for循环是给每个结点初始化
for (i = 0; i < 2*n-1; i++)
{
HF[i].weight = 0;
HF[i].lchild = -1;
HF[i].rchild = -1;
HF[i].parent = -1;
}
//下面的for循环给叶子结点的weight域赋初值
for (i = 0; i < n; i++)
{
printf ("请输入第%d个结点的权值:",i+1);
scanf ("%d", &HF[i].weight);
getchar ();
//吸收回车键
}
}
//找到权值最小的两个节点
void Min (HFMT HF[MAX], int n, int* p, int* r, int j)
{
int Min1 = 9999;
int Min2 = 9999;
int i;
//下面的for循环是为了找到最小的权值所对应的数组下标
for (i = 0; i < j; i++)
{
if (HF[i].parent == -1 && HF[i].weight < Min1)
//HF[i].parent == -1表示未加入哈夫曼树
{
Min1 = HF[i].weight;
*p = i;
}
}
//下面的for循环是为了找到次最小的权值所对应的数组下标
for (i = 0; i < j; i++)
{
if (HF[i].parent == -1 && HF[i].weight < Min2 && HF[i].weight > Min1)
{
Min2 = HF[i].weight;
*r = i;
}
}
}
//哈夫曼树的创建
void CreateHFMT (HFMT HF[MAX], int n)
{
int p, r;
int i, k;
InitHFMT (HF, n);
for (i = n; i < 2*n -1; i++)
{
Min (HF, n, &p, &r, i);
HF[p].parent = i;
HF[r].parent = i;
HF[i].lchild = HF[p].weight;
HF[i].rchild = HF[r].weight;
HF[i].weight = HF[i].lchild + HF[i].rchild;
}
}
void HFnode (HFMT HF[MAX], int i, int j)
{
j = HF[i].parent;
if (HF[j].rchild == HF[i].weight)
//如果它在父母结点的右边
{
printf ("1");
}
else
//如果在左边
{
printf ("0");
}
if (HF[j].parent != -1)
//如果它还有父母结点
{
i = j;
HFnode (HF, i, j);
}
}
void HFMTnode (HFMT HF[MAX], int n)
{
int a, i, j = 0;
printf ("\n输入的权值所对应的哈夫曼代码为:\n");
for (i = 0; i < n; i++)
{
a = i;
printf ("%d的编码为:", HF[i].weight);
HFnode (HF, i, j);
printf ("\n");
i = a;
}
}
int main (void)
{
int n;
HFMT HF[MAX];
printf ("请输入权值的个数:");
scanf ("%d", &n);
CreateHFMT (HF, n);
HFMTnode (HF, n);
return 0;
}