前言
数据结构,一门数据处理的艺术,精巧的结构在一个又一个算法下发挥着他们无与伦比的高效和精密之美,在为信息技术打下坚实地基的同时,也令无数开发者和探索者为之着迷。
也因如此,它作为博主大二上学期最重要的必修课出现了。由于大家对于上学期C++系列博文的支持,我打算将这门课的笔记也写作系列博文,既用于整理、消化,也用于同各位交流、展示数据结构的美。
此系列文章,将会分成两条主线,一条“数据结构基础”,一条“数据结构拓展”。“数据结构基础”主要以记录课上内容为主,“拓展”则是以课上内容为基础的更加高深的数据结构或相关应用知识。
欢迎关注博主,一起交流、学习、进步,往期的文章将会放在文末。
哈夫曼树的背景
每一个传奇的数据结构都会有属于自己的传奇故事
——尤市沃茨基硕的
哈夫曼树的传奇背景,是主角哈夫曼在攻读博士学位期间,修习信息论学科。导师让同学们选择学期考察的方案,可以选择完成报告,或者参加考试。我们的dalao哈夫曼就选择了完成一篇报告。
他遇到的报告题目为:寻找最有效的二进制编码方案
他先是分析了前人的研究结果,发现前辈们的方案都并未很好的解决这个问题,尤其是不能证明其方案是最有效的。相信发现这个问题的哈夫曼当时的心中一定是:
于是他决定先放弃对已有算法的分析,自己研究一个新的算法解决编码问题:
最终,他发明了一个基于有序频率二叉树的编码方案,并很快证明了他是最有效的算法。这个算法,青出于蓝,超过了信息创始人香农和他的导师。哈夫曼使用自底向上的方法构建二叉树,避免了次优算法Shannon-Fano编码的最大弊端──自顶向下构建树。
1952年,哈夫曼将这个发明整理成了学期报告《一种构建极小多余编码的方法》(A Method for the Construction of Minimum-Redundancy Codes)一文,顺利的完成了该科目的学习~~(这要是不过那可真说不过去了)~~ 。现在这种编码方案一般就叫做哈夫曼(Huffman)编码。
哈夫曼的编码方案
哈夫曼遇到的问题,简单来说,需要解决这么几个关键问题:
- 编码不能存在歧义,避免编码的多义性,即不能有某个字符的编码是另一个字符编码的前缀
- 编码应该尽可能的短,这要求采用不等长的编码方案,并将出现频率搞得字符赋予更短的编码
- 编码算法产生的编码方案应该唯一,避免编码与解码的不对应
- 编码算法效率应尽可能的高
哈夫曼定义了一种二叉树,他的构建规则如下
- 对于所有字符,统计其出现的频率。
- 定义二叉树的结点,其中叶子结点的值为各个字符,权值为频率
- 定义结点间的比较规则
-
- 频率为第一关键字小频率优先;
-
- 值为第二关键字,字符大于树内结点
-
- 最早出现位置为第三关键字,更早出现的结点优先
- 每次挑选两个权值最小的结点,创建新的结点作为两节点父亲,父节点的权值为两子节点的和,且较小结点为左儿子,较大者为右儿子
- 重复上述过程,直至集合中剩余一个结点,该节点即为该二叉树的根
依照这种规则建立起来的二叉树,我们称之为哈夫曼树
如下就是一次构建哈夫曼树的过程
哈夫曼的编码方案就是基于这样一颗二叉树进行的。我们规定,所有编码从根节点开始,每次向左走编码尾部追加’0’,向右走编码尾部追加’1’。
在上图中,四个字母的编码方案如下:
- d : 00 d:00 d:00
- a : 01 a:01 a:01
- b : 10 b:10 b:10
- d : 11 d:11 d:11
当然这个方案中四个字符的编码长度相同且有规律,纯属巧合,一般情况下编码长度是不相同的。
哈夫曼树的实现
了解了哈夫曼树的构建过程 ,下面一步我们就需要来想办法实现构建哈夫曼树的过程
首先呢,先来定义一下哈夫曼树的结点类型,同时,我们要重载结点类的小于号用以比较:
class Node{
public:
char c; //结点字符,非字符结点默认为'z' + 1 = '{'
int value; //结点出现频率
int idx; //结点出现最早时间
Node * left;
Node * right;
bool operator < (const Node & node){
if(value == node.value){
return idx < node.idx;
}
return value < node.value
}
};
为了演示哈夫曼树的构建过程,我们拟定一个需求:
给定一个序列仅包含小写字母,统计出每个字符的频率,创建出它的哈夫曼树。
传统的实现方案
传统与优化的解决方案的差别,主要在于如何寻找集合中最小的结点。传统的方案是使用遍历的方式寻找,优化的方案是使用二叉堆来优化这个过程。
话不多说,上代码:
Node * character[26];//二十六个英文字母的结点数组,没出现的字母为空
Node * nodes[1000];//结点集合,每次从中挑选最小结点
int tot = 0;//结点集合规模
char str[10000];
int main(){
cin >> str;
int len = strlen(str);
for(int i = 0,c;i < len;i++){
c = str[i] - 'a';
if(character[c] == NULL){
//如果该字母第一次出现,则创建结点并且记录第一次出现位置
character[c] = new Node();
character[c]->c = str[i];
character[c]->idx = i;
character[c]->value = 1;
character[c]->left = NULL;
character[c]->right = NULL;
nodes[tot++] = character[c];//加入结点集合
}else{
character[c]->value++;
}
}
int cnt = tot;//总结点数量
int first;
int second;
Node * node;
while(cnt-- > 1){
//总结点数量每次减一,总共循环cnt-1次
first = -1;
second = -1;
for(int j = 0;j < tot;j++){
if(nodes[j] == NULL){
continue;
}
if(first == -1 || *nodes[j] < *nodes[first]){
//注意这里是取值进行比较而非直接比较指针
second = first;
first = j;
}else if(second