什么是哈弗曼树
- 百度百科的定义
给定N个权值作为N个叶子结点,构造一棵二叉树,若该树的带权路径长度达到最小,称这样的二叉树为最优二叉树,也称为哈夫曼树(Huffman Tree)。哈夫曼树是带权路径长度最短的树,权值较大的结点离根较近。
哈弗曼树
上述定义很学术,是很严谨的表达,但是看起来总不是那么好理解。在这里,我们不说理论,直接来看一颗哈弗曼树是如何构建的,通过具象事物来理解抽象的概念。
怎样构建哈弗曼树
下边,我们使用一个哈夫曼编码的例子来理解哈弗曼树。
- 背景问题
在这里,我们通过一个示例来说明。
现在,需要将一篇英文文章从A发送给B,要求是编码的长度最短。英文字母一共26个,大小写如果不同,那就是52个。那么我们需要6位二进制来进行编码(2^5 < 52 < 2 ^6=64)。如果这篇文章有字母10000个,那么编码长度就是10000*6。我们知道,在文章中,每一个字母出现的频率是不同的,思考:对不同频率的字母使用不通长度的编码位数,也就是出现频率最高的字母最短的编码长度,给频率最低的字母最长的编码长度。 这样就能使得整篇文章的编码总长度降到最低。提高传输效率。
实现方式如下:
字符频率表
字母 | A | B | C | D | E | F | G | H |
---|---|---|---|---|---|---|---|---|
频率 | 80 | 30 | 20 | 75 | 40 | 8 | 55 | 60 |
根据上述表格,我们知道,A的编码是最短的,F的编码是最长的。这里的频率是我任意指定的,实际字母出现的概率在密码学中有统计(可以参考字母频率)。
- 构建
第一步:选出其中频率最小的两个字母F和C,用这连个字母组成二叉树,小的为左孩子,大的为右孩子。并且将F和C的频率之和作为根节点,返回给频率表。
第二步:连续重复上述操作。
F+C 小于 B,所以28在左孩子的位置。
字母 | A | B | D | E | FC | G | H |
---|---|---|---|---|---|---|---|
频率 | 80 | 30 | 75 | 40 | 28 | 55 | 60 |
发现此时最小的是 40 和 55 (E和G)
字母 | A | D | E | FCB | G | H |
---|---|---|---|---|---|---|
频率 | 80 | 75 | 40 | 58 | 55 | 60 |
此时最小的是58 和60 (EG和H)
字母 | A | D | FCB | EG | H |
---|---|---|---|---|---|
频率 | 80 | 75 | 58 | 95 | 60 |
字母 | A | D | FCBH | EG |
---|---|---|---|---|
频率 | 80 | 75 | 118 | 95 |
字母 | AD | FCBH | EG |
---|---|---|---|
频率 | 155 | 118 | 95 |
字母 | AD | FCBHEG |
---|---|---|
频率 | 155 | 213 |
至此,哈弗曼树构造完成了,那么前面说的编码是怎么实现的呢?按照二进制,输的左边标0,右边标1.沿着树的方向直至字母所在的叶子节点的0和1的序列即为该字母的哈夫曼编码。如下如:
下表是所有字母的最终编码
字母 | 编码 |
---|---|
A | 01 |
B | 1101 |
C | 11001 |
D | 00 |
E | 100 |
F | 11000 |
G | 101 |
H | 111 |
假如我要传送ABC四个字母,那么编码就是0 111111 1111101,一共14位,如果按照最开始的一个字母6位编码,那么长度就是18了。
哈弗曼树的代码实现
构建哈夫曼树时,需要每次根据各个结点的权重值,筛选出其中值最小的两个结点,然后构建二叉树。
查找权重值最小的两个结点的思想是:从树组起始位置开始,首先找到两个无父结点的结点(说明还未使用其构建成树),然后和后续无父结点的结点依次做比较,有两种情况需要考虑:
- 如果比两个结点中较小的那个还小,就保留这个结点,删除原来较大的结点;
- 如果介于两个结点权重值之间,替换原来较大的结点;
哈夫曼树的结构数据结构
// 哈夫曼树结点结构
typedef int Type;
typedef struct HuffmanNode_
{
Type weight; // 节点权重
Type parent, left, right; //父结点、左孩子、右孩子在数组中的位置下标
}Node, *HuffmanTree;
// 选中频率最小的两个数据
// HT数组中存放的哈夫曼树,end表示HT数组中存放结点的最终位置,s1和s2传递的是HT数组中权重值最小的两个结点在数组中的位置
void select(HuffmanTree HT, int *pos1, int *pos2, int end)
{
int min1 = 0, min2 = 0;
int i = 1; // 数组的 0 号元素作为根节点的位置所以不使用
// 找到没有构建成树的第一个节点
while (HT[i].parent != 0 && i <= end)
{
i++;
}
min1 = HT[i].weight;
*pos1 = i;
i++;
// 找到没有构建成树的第二个节点
while(HT[i].parent != 0 && i <= end)
{
i++;
}
min2 = HT[i].weight;
if (min2 < min1)
{
min2 = min1;
*pos2 = *pos1;
min1 = HT[i].weight;
*pos1 = i;
}
else
{
*pos2 = i;
}
// 取得两个节点之后,跟之后所有没有构建成树的节点逐一比较,最终获取最小的两个节点
for (int j = i+1; j <= end; ++j)
{
// 如果已经存在父节点,也就是已经被构建树了,则跳过
if (HT[j].parent != 0)
{
continue;
}
// 如果比min1 还小,将min2 = 敏, min1修改为新的节点下标
if (HT[j].weight < min1)
{
min2 = min1;
min1 = HT[j].weight;
*pos2 = *pos1;
*pos1 = j;
}
else if (HT[j].weight < min2 && HT[j].weight > min1)
{
// 如果大于 min1 小于 min2
min2 = HT[j].weight;
*pos2 = j;
}
}
}
// 创建完整的哈夫曼树
// HT为地址传递的存储哈夫曼树的数组,w为存储结点权重值的数组,n为结点个数
HuffmanTree init_huffman_tree(Type *weight, int node_num)
{
if (node_num <= 1)
{
// 只有一个节点那么编码就是 0
return NULL;
}
int tree_node_num = node_num * 2 - 1; // 根节点不使用
HuffmanTree p = (HuffmanTree)malloc((tree_node_num+1) * sizeof(Node));
// 初始化哈夫曼数组中的所有节点
for (int i = 1; i <= tree_node_num; ++i)
{
if (i <= node_num)
{
(p+i)->weight = *(weight+i-1); // 第0个位置不使用
}
else
{
(p+i)->weight = 0;
}
(p+i)->parent = 0;
(p+i)->left = 0;
(p+i)->right = 0;
}
return p;
}
void close_huffman_tree(HuffmanTree HT)
{
if (HT)
{
free(HT);
HT = NULL;
}
}
void create_huffman_tree(HuffmanTree HT, int node_num)
{
if (NULL == HT || node_num <= 1)
{
return;
}
int tree_node_num = node_num * 2 - 1; // 根节点不使用
for (int i = node_num + 1; i <= tree_node_num; ++i)
{
int pos1 = -1, pos2 = -1;
// 找到频率最小的连个节点
select(HT, &pos1, &pos2, i-1);
printf("当前最小的两个节点 [%d %d]\n", HT[pos1].weight, HT[pos2].weight);
// 这里使用下表来表示父子关系
HT[pos1].parent = HT[pos2].parent = i; // pos1 位置的元素和pos2位置的元素 的父节点就是,第 i个位置的元素
HT[i].left = pos1; // 父节点的左后孩子赋值
HT[i].right = pos2;
HT[i].weight = HT[pos1].weight + HT[pos2].weight; // 父节点的权重等于 左右孩子权重的和
}
}
- 测试代码
void print(HuffmanTree HT, int node_num)
{
if (NULL == HT)
{
printf("数组为空\n");
return;
}
int tree_node_num;
for (int i = 1; i < tree_node_num; ++i)
{
printf("%d 的父节点:%d 左孩子:%d 右孩子:%d\n", HT[i].weight, HT[HT[i].parent].weight, HT[i].left, HT[i].right);
}
}
int main(int argc, char const *argv[])
{
Type weight[8] = {80, 30, 20, 75, 40, 8, 55, 60};
int node_num = sizeof(weight) / sizeof(Type);
HuffmanTree HT = init_huffman_tree(weight, node_num);
create_huffman_tree(HT, node_num);
print(HT, node_num);
close_huffman_tree(HT);
return 0;
}
- 测试结果
!
为什么要设计哈弗曼树
- 哈弗曼树主要用于哈夫曼编码,其主要作用就是利用频率属性进行编码,最终达到目的:让高频的数据拥有短编码,而低频的数据拥有长编码。
- 哈夫曼编码并不是适用于所有场景,它更适用于频率变化多端的数据编码。