一.赫夫曼树
1.概念
首先,我们得了解一下几个概念:
路径
:一个结点到另一个结点之间的路径。
路径长度
:路径上的分支数目叫路径长度
。
带权路径长度
:所有叶子结点的带权路径长度之和。
如下图,该树的路径长度是:1+2+3+3=9
而带权路径则是:1*7+2*5+3+2+3*4=35
2.实现
那么,我们如何构造一棵赫夫曼树呢?不难得出,肯定是权越大的结点,它的路径长度应该最短。按以下步骤:
- 根据权值,将每个结点记作一棵树,记为F1 · · ·Fn,它的左右儿子皆为空,值为它的权。
- 将这N棵中,选两个最小的树,构成一棵新的树,该树的左右儿子为这两个结点,该树的值为这两个结点权的和。并在N棵树中,删掉这两颗树,并添加构造出来的新树。
- 重复2这个动作,直到只剩下一棵树。该树即为我们的赫夫曼树。
二.赫夫曼编码
1.背景
在我们通信中,信息几乎全是以二进制编码的形式发送。假设我们对ABCD
进行编码,有00 01 10 11
这样编码。但如果为了降低传输延迟和资源,我们可以对编码的位数进行限制,将ABCD
编码为0 00 1 01
,这样就可以少几位。但是这样会出现一个问题,假设我们收到了0000
,那么这串编码就会出现歧义,我们可以理解为ABA、BAA、AAB、AAAA、BB
。所以我们需要一种任意一个字符的编码都不是另一个字符的编码的前缀
,这种编码称为前缀编码
。
此时,赫夫曼编码
出现惹。
2.原理
我们将一颗赫夫曼树的左儿子
规定为0,右儿子
规定为1,那么一颗赫夫曼树,可以完成一个编码:
此时,如果我们能够统计每个字符的出现频率,并按频率作为这个字符的权,那么通过构造这些字符的赫夫曼树,就可以实现最短前缀编码
。
三.代码实现
#include <stdio.h>
#include <stdlib.h>
#define OK 1
#define ERROR 0
typedef struct Tnode {
int weight;
struct Tnode *front;
struct Tnode *next;
struct Tnode *lc;
struct Tnode *rc;
}Tnode;
// 为了在寻找最小结点的时候
//只用一次遍历就找到最小的两个
//采用双向链表的方式比较好储存
Tnode *TreeInit(void);
int TreeListShow(Tnode *list);
Tnode *TreeMake(Tnode *list);
int HCodeShow(Tnode *root, int deep);
int HFcode[100] = {
0}; // 用于存放赫夫曼编码的数组
char HFstr[100] = {
0}; // 用于存放值与权重对应的hash数组
int main()
{
Tnode *list = TreeInit();
Tnode *hftree;
//TreeListShow(list);
hftree = TreeMake(list);
HCodeShow(hftree, 0);
return 0;
}
Tnode *TreeInit(void)
{
int len, i, temp;
char tstr;
Tnode *pre, *post;
//初始化双向链表头
Tnode *list = (Tnode*)malloc(sizeof(Tnode));
list->front = NULL;
list->weight = -1;
//pre为了便于循环赋值
pre = list;
printf("please input the number of the weights:");
scanf("%d", &len);
printf("please input the char-weights(int) like A1,B2,:\n");
fflush(stdin); // 清空缓存区,否则%c会读到换行符
for (i = 0; i < len; ++i)
{
scanf("%c%d,", &tstr, &temp);
HFstr[temp] = tstr; //hasp表,HFstr[权重]=字符
// 创建后结点
post = (Tnode*)malloc(sizeof(Tnode));
// 构造双向链表
pre->next = post;
post->next = NULL;
// 初始化结点
post->weight = temp;
post->front = pre;
post->lc = NULL;
post->rc = NULL;
// 更新结点指针,都向后移一位
pre = post;
post = pre->next;
}
return list;
}
int TreeListShow(Tnode *list)
{
Tnode *thisNode = list->next;
printf("The Weights are:\n");
// 遍历输出当前双向链表里的权重信息
while(thisNode != NULL)
{
printf("%d ", thisNode->weight);
thisNode = thisNode->next;
}
printf("\n");
return OK;
}
Tnode *TreeMake(Tnode *list)
{
Tnode *thisNode, *newNode;
Tnode *mmin, *min;
Tnode *pre, *pos;
// 循环结束的条件是,双向链表里只有一个有效结点了,所以两个next应该为NULL就结束
while(list->next->next != NULL)
{
thisNode = list->next->next; // 遍历指针,从第二个结点开始
mmin = list->next; // 赋初值,最小的结点指针为第一个结点
min = list->next->next; // 赋初值,次小的结点指针为第二个结点
while(thisNode != NULL) // 单次遍历结束条件
{
if(thisNode->weight < mmin->weight) // 如果该结点比最小的结点还小
{
min = mmin; // 次小的结点为刚才最小的结点
mmin = thisNode; // 最小的结点为该结点
}
else if (thisNode->weight < min->weight) // 如果该结点小于次小结点而大于最小结点
{
min = thisNode; // 次小结点为这个
}
thisNode = thisNode->next; // 更新遍历指针
}
// 一次循环完后,应该存在最小 次小值
// 删掉最小结点
pre = mmin->front;
pos = mmin->next;
pre->next = pos;
// 要考虑最小结点是否是最后一个 否则会越界
if(pos != NULL) pos->front = pre;
// 同理删掉次小结点
pre = min->front;
pos = min->next;
pre->next = pos;
if(pos != NULL) pos->front = pre;
// 生成新结点
newNode = (Tnode*)malloc(sizeof(Tnode));
newNode->lc = mmin;
newNode->rc = min;
newNode->weight = mmin->weight + min->weight;
// 把新结点插入到第一个结点
pos = list->next;
list->next = newNode;
newNode->front = list;
newNode->next = pos;
// 也要考虑是否为最后一个结点
if (pos != NULL) pos->front = newNode;
// 取消注释下面这句话,就可以看到每次迭代后的效果
//TreeListShow(list);
}
return list->next;
}
int HCodeShow(Tnode *root, int deep)
{
int i;
if (root->lc != NULL || root->rc != NULL)
{
// 左结点非空,遍历
if (root->lc != NULL)
{
HFcode[deep] = 0; // 当前深度的编码为0
HCodeShow(root->lc, deep+1);
}
// 右结点非空,遍历
if (root->rc != NULL)
{
HFcode[deep] = 1; // 当前深度的编码为1
HCodeShow(root->rc, deep+1);
}
}
else
{
// 这里是叶子了,可以输出了
// 根据hash表输出当前字符
printf("\n%c is coded as:", HFstr[root->weight]);
// 根据当前深度,输出编码值
for (i = 0; i < deep; ++i)
{
printf("%d", HFcode[i]);
}
}
}