哈夫曼编码

——牛客<美团点评>笔试【编程题】 字符编码


  最近,春招实习生开始了,相信大家都开始准备各种面试、笔试了。
  博主也不例外,走上了起早贪黑,看书、刷题的不归路。作为人生的第一篇博客(之前确实太懒,这次就用哈夫曼编码作为我Blog的Hellow world吧),在这里给大家带来一篇 哈夫曼编码 相关知识的讲解与代码分享,同时也作为一个算法笔记,有兴趣的同学可以来看看,有写的不对的地方欢迎指出,祝大家都能拿到一个好的offer。


首先是 哈夫曼编码 的一些理论基础

  哈夫曼树又称最优二叉树,是一种带权路径长度最短的二叉树。所谓树的带权路径长度,就是树中所有的叶结点的权值乘上其到根结点的路径长度(若根结点为0层,叶结点到根结点的路径长度为叶结点的层数)。树的带权路径长度记为WPL=(W1L1+W2L2+W3L3+…+ WnLn),N个权值Wi(i=1,2,…n)构成一棵有N个叶结点的二叉树,相应的叶结点的路径长度为Li(i=1,2,…n)。可以证明哈夫曼树的WPL是最小的。
构造哈夫曼树的算法如下:
1)对给定的n个权值{W1,W2,W3,…,Wi,…,Wn}构成n棵二叉树的初始集合F={T1,T2,T3,…,Ti,…, Tn},其中每棵二叉树Ti中只有一个权值为Wi的根结点,它的左右子树均为空。
2)在F中选取两棵根结点权值最小的树作为新构造的二叉树的左右子树,新二叉树的根结点的权值为其左右子树的根结点的权值之和。
3)从F中删除这两棵树,并把这棵新的二叉树同样以升序排列加入到集合F中。
4)重复2)和3),直到集合F中只有一棵二叉树为止。

  简而言之,就是要先知道一系列的权值信息(比如说要编码字符串的每个字符出现频率),然后把它们分别作为哈夫曼树中各个叶子节点的权值,接下来呢就需要由叶子节点来自下往上构造哈夫曼树了。
  构造的思路是这样的:所有的叶子节点构成了最初的森林(所有树的集合),我们可以使用priority_queue对这些树进行管理,使得集合中的数据始终保持着递增的顺序。然后,每次就让前两个元素出队,再构造一个新的节点(树),其权值为出队的两个元素权值之和,左子树为前面第一个元素,右子树为前面第二个元素,这样集合中元素的个数-1。如此做,直至集合中只剩余一个节点(根节点)为止,此时仅有的这棵树便是哈夫曼树。


以美团的这题为例

输入

MT-TECH-TEAM

输出

33


具体编码流程如下

初始状态

执行1次

执行2次

执行3次

执行4次

执行5次

完成哈夫曼树

至此,哈夫曼树完成构造,接下来只需要遍历统计即可。


附上具体实现代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
#include<iostream>
#include<stdlib.h>
#include<string.h>
#include<stdio.h>
#include<algorithm>
using namespace std;
/**哈夫曼编码思想**/
typedef struct Node
{
char typeChar;
int rate;
Node *leftChild;
Node *rightChild;
}charNode;
//存放输入的字符种类
char *inputStr;
//存放字符节点的数组
Node nodes[200 + 199];
//总的节点数
int total;
//记录当前所处的深度
int depth;
//统计数量
int getNum(char *in, char c)
{
int times = 0;
for (int i = 0; i <= strlen(in); i++)
{
if (*(in + i) == c) times++;
}
return times;
}
//统计输入的字符种类(当时写的这方法不好,个人推荐使用hash存储的方式统计)
int getTypeChar(char *in)
{
inputStr = (char *)malloc(sizeof(char)*200);
memset(inputStr, '\0', sizeof(inputStr));
int p = 0;
bool isAdded;
//cout<<strlen(in)<<endl;
for (int i = 0; i<strlen(in); i++)
{
isAdded = false;
if (p == 0)
{
*inputStr = *in;
nodes[p].typeChar = *inputStr;
nodes[p].rate = getNum(in, *in);
nodes[p].leftChild = NULL; //叶节点
nodes[p].rightChild = NULL; //叶节点
p++;
continue;
}
for (int j = 0; j<p; j++)
{
if (*(inputStr + j) == *(in + i))
{
isAdded = true;
break;
}
}
if (!isAdded)
{
inputStr[p] = *(in + i);
nodes[p].typeChar = *(inputStr + p);
nodes[p].rate = getNum(in, *(in + i));
nodes[p].leftChild = NULL; //叶节点
nodes[p].rightChild = NULL; //叶节点
p++;
}
}
return p;
}
//给排序函数使用的比较函数
bool compNode(const Node & node1, const Node & node2)
{
if (node1.rate <= node2.rate)
{
return true;
}
else
{
return false;
}
}
//哈夫曼编码
void huf(int startIndex, int endIndex, int length)
{
sort(nodes + startIndex, nodes + startIndex + length, compNode);
nodes[endIndex + 1].rate = nodes[startIndex].rate + nodes[startIndex+1].rate;
nodes[endIndex + 1].leftChild = &nodes[startIndex];
nodes[endIndex + 1].rightChild = &nodes[startIndex+1];
if (endIndex + 1 != total)
{
huf(startIndex + 2, endIndex + 1, length - 1);
}
}
int depthFirst(charNode *);
//遍历哈夫曼树,计算出编编码长度
int getCodeLength(int lastIndex)
{
charNode *root = &nodes[lastIndex];
depth = -1;
return depthFirst(root);
}
//递归遍历
int depthFirst(charNode *node)
{
depth++;
if (node->leftChild == NULL && node->rightChild == NULL)
{
return node->rate * depth;
}
else
{
int leftSize = depthFirst(node->leftChild);
depth--;
int rightSize = depthFirst(node->rightChild);
depth--;
return leftSize + rightSize;
}
}
int main()
{
char *in;
int num; //不同字符的数量
in = (char *)malloc(sizeof(char) * 1000);
while(scanf("%s", in) != EOF)
{
num = getTypeChar(in);
if(num == 1)
{
cout<<strlen(in)<<endl;
continue;
}
total = num * 2 - 1;
huf(0, num - 1, num);
cout<<getCodeLength(total-1)<<endl;
}
return 0;
}
  • 如果只是为了AC也不需要写这么复杂。不过此处主要考虑到对 哈夫曼编码 的理解,所以代码方面就自己构造结构体、指针去实现了

另外代码中用到的二叉树一个重要性质

    对于任意二叉树,设拥有叶子结点个,二度节点b个,则满足:

$$ a = b + 1 $$