前言

数据结构,一门数据处理的艺术,精巧的结构在一个又一个算法下发挥着他们无与伦比的高效和精密之美,在为信息技术打下坚实地基的同时,也令无数开发者和探索者为之着迷。

也因如此,它作为博主大二上学期最重要的必修课出现了。由于大家对于上学期C++系列博文的支持,我打算将这门课的笔记也写作系列博文,既用于整理、消化,也用于同各位交流、展示数据结构的美。

此系列文章,将会分成两条主线,一条“数据结构基础”,一条“数据结构拓展”。“数据结构基础”主要以记录课上内容为主,“拓展”则是以课上内容为基础的更加高深的数据结构或相关应用知识。

欢迎关注博主,一起交流、学习、进步,往期的文章将会放在文末。


哈夫曼树的背景

每一个传奇的数据结构都会有属于自己的传奇故事
——尤市沃茨基硕的

哈夫曼树的传奇背景,是主角哈夫曼在攻读博士学位期间,修习信息论学科。导师让同学们选择学期考察的方案,可以选择完成报告,或者参加考试。我们的dalao哈夫曼就选择了完成一篇报告。

他遇到的报告题目为:寻找最有效的二进制编码方案

他先是分析了前人的研究结果,发现前辈们的方案都并未很好的解决这个问题,尤其是不能证明其方案是最有效的。相信发现这个问题的哈夫曼当时的心中一定是:

于是他决定先放弃对已有算法的分析,自己研究一个新的算法解决编码问题:

最终,他发明了一个基于有序频率二叉树的编码方案,并很快证明了他是最有效的算法。这个算法,青出于蓝,超过了信息创始人香农和他的导师。哈夫曼使用自底向上的方法构建二叉树,避免了次优算法Shannon-Fano编码的最大弊端──自顶向下构建树。

1952年,哈夫曼将这个发明整理成了学期报告《一种构建极小多余编码的方法》(A Method for the Construction of Minimum-Redundancy Codes)一文,顺利的完成了该科目的学习~~(这要是不过那可真说不过去了)~~ 。现在这种编码方案一般就叫做哈夫曼(Huffman)编码。

哈夫曼的编码方案

哈夫曼遇到的问题,简单来说,需要解决这么几个关键问题:

  • 编码不能存在歧义,避免编码的多义性,即不能有某个字符的编码是另一个字符编码的前缀
  • 编码应该尽可能的短,这要求采用不等长的编码方案,并将出现频率搞得字符赋予更短的编码
  • 编码算法产生的编码方案应该唯一,避免编码与解码的不对应
  • 编码算法效率应尽可能的高

哈夫曼定义了一种二叉树,他的构建规则如下

  • 对于所有字符,统计其出现的频率。
  • 定义二叉树的结点,其中叶子结点的值为各个字符,权值为频率
  • 定义结点间的比较规则
    • 频率为第一关键字小频率优先;
    • 值为第二关键字,字符大于树内结点
    • 最早出现位置为第三关键字,更早出现的结点优先
  • 每次挑选两个权值最小的结点,创建新的结点作为两节点父亲,父节点的权值为两子节点的和,且较小结点为左儿子,较大者为右儿子
  • 重复上述过程,直至集合中剩余一个结点,该节点即为该二叉树的根

依照这种规则建立起来的二叉树,我们称之为哈夫曼树

如下就是一次构建哈夫曼树的过程

哈夫曼的编码方案就是基于这样一颗二叉树进行的。我们规定,所有编码从根节点开始,每次向左走编码尾部追加’0’,向右走编码尾部追加’1’。

在上图中,四个字母的编码方案如下:

  • d:00d:00d:00
  • a:01a:01a:01
  • b:10b:10b:10
  • d:11d:11d: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 == -1 || *nodes[j] < *nodes[second]){second = j;}}//创建新的结点node = new Node();node->c = 'z' + 1;node->value = nodes[first]->value + nodes[second]->value;node->left = nodes[first];node->right = nodes[second];node->idx = len++;nodes[tot++] = node;//添加新结点nodes[first] = NULL;//删除合并的结点nodes[second] = NULL;}Node * root = nodes[tot - 1];//哈夫曼树根节点为最后加入集合的结点
}

使用堆优化

在前面的章节,我们介绍了二叉堆,这是个能够快速存取最值的结构,在这里正是它大显身手的地方。

如果读者此时还没有掌握二叉堆,可以先回去看一看,或者暂时跳过这段内容,不影响后续的阅读

每次我们可以从堆中拿去两个最小节点(堆顶取一次,弹一次,再取一次,再弹一次)。创建一个新节点作为父节点,并将它存入堆中。

在具体实现之前,我们有必要先将二叉堆写出来:

//C
Node * heap[2000];//堆得存储
int tot = 0;//堆得规模void down(int k){int son = k << 1;while(son <= tot){if(son + 1 <= tot && *heap[son + 1] < *heap[son]){son++;}if(*heap[k]  < *heap[son]){break;}Node * temp = heap[k];heap[k] = heap[son];heap[son] = temp;k = son;son <<= 1;}
}void up(int k){int fa = k >> 1;while(fa != 0){if(*heap[fa] < *heap[k]){break;}Node * temp = heap[k];heap[k] = heap[fa];heap[fa] = temp;k = fa;fa >>= 1;}
}void add(Node * node){heap[++tot] = node;up(tot);
}Node * peak(){return heap[1];
}void pop(){heap[1] = heap[tot--];down(1);
}

堆实现了之后,我们使用堆再来进行构建的过程

Node * character[26];//二十六个英文字母的结点数组,没出现的字母为空
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;add(character[c]);}else{character[c]->value++;}}int cnt = tot;//总结点数量Node * first;Node * second;Node * node;while(size > 1){//总结点数量每次减一,总共循环cnt-1次first = peak();pop();second = peak();pop();//创建新的结点node = new Node();node->c = 'z' + 1;node->value = first->value + second->value;node->left = first;node->right = second;node->idx = len++;add(node);}Node * root = peak();//哈夫曼树根节点
}

编码与译码

获取编码

编码方案在上文描述算法的时候我们有提到过。要获取所有的字符编码,我们需要遍历整颗哈夫曼树,对于所有存放字符的叶子结点,应该获取并记录其编码。

那么首先,我们应该有一个存放编码的地方:

char * code[26];//26个小写字母的编码

接着,来遍历这棵树。当然,遍历整棵树获取编码的方式有很多,这里博主仅提供一种可供参考的方案:

void encode(char * s,int dep,Node * node){//当前编码,深度,当前结点if(node->left == NULL && node->right == NULL){//如果该结点为叶子结点,则获取编码int c = node->c - 'a';s[dep] = '\0';code[c] = strdup(s);//根据当前编码克隆一个字符串并储存return;}s[dep] = '0';encode(s,dep + 1,node->left);//遍历左子树,编码追加0s[dep] = '1';encode(s,dep + 1,node->right);//遍历右子树,编码追加1
}

完成编码之后,在主函数中调用一下:

 Node * root;...//获取根节点char s[20];encode(s,0,root);for(int i = 0;i < 26;i++){//打印出出现字母的编码if(code[i] != NULL){cout << (char)(i + 'a') << ":" << code[i] << endl;}}

进行译码

一般来说,译码工作是在哈夫曼树构建成功基础上,给定一个编码序列,将其翻译成为源码,如果无法翻译则给予提示。

例如,对于刚才展示的运行示例,如果给定编码序列为:110110101110110101110110101
则译码序列为:aabaabaab
若为:110110101101101011011010,则这个序列存在问题,因为没有字符编码为10

译码的过程本质上是在哈夫曼树上跟着序列进行模拟

  • 如果当前结点对应位为0,则下一位跳转到左结点
  • 如果当前结点对应位为1,则下一位跳转到右结点
  • 如果当前结点为叶子结点,则获得一位译码。当前结点指向根节点,重复上述过程
  • 如果当前结点对应最后一位编码且非叶子结点,说明编码存在问题。

用代码实现以下:

char * decode(Node * root,char * target){//根节点,原码char result[50];//结果序列int resultCnt = 0;//结果长度int targetCnt = strlen(target);//当前对应原码下标Node * curr = root;//当前结点for(int i = 0;i < targetCnt;i++){if(target[i] == '0'){curr = curr->left;    }else{curr = curr->right;}if(curr->left == NULL && curr->right == NULL){//如果当前结点为叶子结点,获取译码result[resultCnt++] = curr->c;curr = root;//当前结点指向头结点}}if(curr != root){//译码结束后不指向头结点,原码存在错误return "INVALID";}else{//原码有效,返回结果result[resultCnt] = '\0';return strdup(result);//克隆结果字符串返回}
}

在主函数中调用测试这个函数:

Node * root;
char str[100];
...
while(true){cout << "请输入原码:";cin >> str;cout << "译码结果为:" << decode(root,str) << endl;
}

跑几组数据试一试:

ok没有问题~

完整的堆实现的代码这里再放一下:

#include<iostream>
#include<cstring>
using namespace std;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 * heap[2000];
int tot = 0;void down(int k){int son = k << 1;while(son <= tot){if(son + 1 <= tot && *heap[son + 1] < *heap[son]){son++;}if(*heap[k]  < *heap[son]){break;}Node * temp = heap[k];heap[k] = heap[son];heap[son] = temp;k = son;son <<= 1;}
}void up(int k){int fa = k >> 1;while(fa != 0){if(*heap[fa] < *heap[k]){break;}Node * temp = heap[k];heap[k] = heap[fa];heap[fa] = temp;k = fa;fa >>= 1;}
}void add(Node * node){heap[++tot] = node;up(tot);
}Node * peak(){return heap[1];
}void pop(){heap[1] = heap[tot--];down(1);
}char * code[26];void encode(char * s,int dep,Node * node){//当前编码,深度,当前结点if(node->left == NULL && node->right == NULL){//如果该结点为叶子结点,则获取编码int c = node->c - 'a';s[dep] = '\0';code[c] = strdup(s);//根据当前编码克隆一个字符串并储存return;}s[dep] = '0';encode(s,dep + 1,node->left);//遍历左子树,编码追加0s[dep] = '1';encode(s,dep + 1,node->right);//遍历右子树,编码追加1
}char * decode(Node * root,char * target){//根节点,原码char result[50];//结果序列int resultCnt = 0;//结果长度int targetCnt = strlen(target);//当前对应原码下标Node * curr = root;//当前结点for(int i = 0;i < targetCnt;i++){if(target[i] == '0'){curr = curr->left;  }else{curr = curr->right;}if(curr->left == NULL && curr->right == NULL){//如果当前结点为叶子结点,获取译码result[resultCnt++] = curr->c;curr = root;//当前结点指向头结点}}if(curr != root){//译码结束后不指向头结点,原码存在错误return "INVALID";}else{//原码有效,返回结果result[resultCnt] = '\0';return strdup(result);//克隆结果字符串返回}
}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;add(character[c]);}else{character[c]->value++;}}int cnt = tot;//总结点数量Node * first;Node * second;Node * node;while(tot > 1){//总结点数量每次减一,总共循环cnt-1次first = peak();pop();second = peak();pop();//创建新的结点node = new Node();node->c = 'z' + 1;node->value = first->value + second->value;node->left = first;node->right = second;node->idx = len++;add(node);}Node * root = peak();//哈夫曼树根节点char s[20];encode(s,0,root);for(int i = 0;i < 26;i++){if(code[i] != NULL){cout << (char)(i + 'a') << ":" << code[i] << endl;}}while(true){cout << "请输入原码:";cin >> str;cout << "译码结果为:" << decode(root,str) << endl;}
}

往期博客

  • 【数据结构基础】数据结构基础概念
  • 【数据结构基础】线性数据结构——线性表概念 及 数组的封装
  • 【数据结构基础】线性数据结构——三种链表的总结及封装
  • 【数据结构基础】线性数据结构——栈和队列的总结及封装(C和java)
  • 【算法与数据结构基础】模式匹配问题与KMP算法
  • 【数据结构与算法基础】二叉树与其遍历序列的互化 附代码实现(C和java)
  • 【数据结构与算法拓展】 单调队列原理及代码实现
  • 【数据结构基础】图的存储结构
  • 【数据结构与算法基础】并查集原理、封装实现及例题解析(C和java)
  • 【数据结构与算法拓展】二叉堆原理、实现与例题(C和java)

参考资料:

  • 《数据结构》(刘大有,杨博等编著)
  • 《算法导论》(托马斯·科尔曼等编著)
  • 《图解数据结构——使用Java》(胡昭民著)
  • OI WiKi

【数据结构与算法基础】哈夫曼树与哈夫曼编码(C++)相关推荐

  1. 数据结构和算法基础(4)——树

    一.概念 1. 为什么需要树这种数据结构 ① 数组存储方式 优点:通过下标方式访问元素,速度快.对于有序数组可以通过二分查找提高检测速度. 缺点:如果要通过内容来查找元素的位置,或者插入删除值时,效率 ...

  2. 数据结构与算法基础-学习-19-哈夫曼解码

    一.个人理解 哈夫曼树和哈夫曼编码相关概念.代码.实现思路分享,请看之前的文章链接<数据结构与算法基础-学习-17-二叉树之哈夫曼树>.<数据结构与算法基础-学习-18-哈夫曼编码& ...

  3. 《数据结构与算法》(十一)- 树、森林与二叉树的转换及哈夫曼树详解

    目录 前言 1. 树.森林与二叉树之间的转换 1.1 树转换为二叉树 1.2. 森林转换为二叉树 1.3. 二叉树转换为树 1.4 二叉树转换为森林 1.5 树与森林的遍历 2. 哈夫曼树及其应用 2 ...

  4. 【Java数据结构与算法】第十二章 哈夫曼树和哈夫曼编码

    第十二章 哈夫曼树和哈夫曼编码 文章目录 第十二章 哈夫曼树和哈夫曼编码 一.哈夫曼树 1.基本术语 2.构建思路 3.代码实现 三.哈夫曼编码 1.引入 2.介绍 3.代码实现哈夫曼编码综合案例 一 ...

  5. 【数据结构】树与树的表示、二叉树存储结构及其遍历、二叉搜索树、平衡二叉树、堆、哈夫曼树与哈夫曼编码、集合及其运算

    1.树与树的表示 什么是树? 客观世界中许多事物存在层次关系 人类社会家谱 社会组织结构 图书信息管理 分层次组织在管理上具有更高的效率! 数据管理的基本操作之一:查找(根据某个给定关键字K,从集合R ...

  6. 【数据结构与算法基础】树与二叉树的互化

    前言 数据结构,一门数据处理的艺术,精巧的结构在一个又一个算法下发挥着他们无与伦比的高效和精密之美,在为信息技术打下坚实地基的同时,也令无数开发者和探索者为之着迷. 也因如此,它作为博主大二上学期最重 ...

  7. 【数据结构与算法基础】树的应用

    写在前面 树这一数据结构学的差不多了,该拉出来练练了.本节学习几个树的应用,包括优先队列.Huffman编码等. 1.优先队列(Priority Queue) 优先队列是特殊的"队列&quo ...

  8. 【数据结构】【哈夫曼树】哈夫曼树、赫夫曼树(Huffman Tree)C语言实现

    目录 一.哈夫曼树定义与原理 二.构建哈夫曼树 三.哈夫曼编码 完整代码: 前言:章末含c语言实现完整代码 一.哈夫曼树定义与原理 哈夫曼树又称最优二叉树,是一种带权路径长度最短的二叉树.所谓树的带权 ...

  9. 数据结构图文解析之:哈夫曼树与哈夫曼编码详解及C++模板实现

    0. 数据结构图文解析系列 数据结构系列文章 数据结构图文解析之:数组.单链表.双链表介绍及C++模板实现 数据结构图文解析之:栈的简介及C++模板实现 数据结构图文解析之:队列详解与C++模板实现 ...

最新文章

  1. JMS - QueueBrowser
  2. FTServer 1.1 发布,多语言全文搜索服务器
  3. 读书笔记《单核工作法》1
  4. COJ1005(Binary Search Tree analog)
  5. 翻身做主--给自己的软件制作注册码
  6. 长沙.NET技术社区·设计到实现
  7. Android怎么自定义listview布局,Android ListView自定义布局
  8. c语言程序设计中三子棋游戏,C语言实现简易版三子棋游戏
  9. button 样式_缩减 SCSS 50%样式代码的 14 条实战经验
  10. 《剑指Offer》 二维数组中的查找
  11. element UI Cascader 级联选择器 动态加载次级选项
  12. 《机器学习实战》学习笔记
  13. 如何删除windows服务zz 重新安装PostgreSQL时删除上次遗留service的方法
  14. Dlib库实现人脸关键点检测(Opencv实现)
  15. win7共享文件服务器设置密码,Win7局域网共享文件夹添加密码保护文件安全的方法...
  16. java爬虫入门第二弹——通过URL下载图片(以下载百度logo为例)
  17. Spark面试题及其答案
  18. 2021寒假MISC打卡DAY1
  19. CINTA作业一:加减乘除
  20. 【Java版算法思想】双指针算法

热门文章

  1. Hadoop2.x一次分布式HA启动时 ERROR: Cannot set priority of xxxx process 解决方式
  2. 最详尽的4K技术科普
  3. 可能是稀土掘金最好学的jsoup示范教程
  4. Acm+java各种数据结构_Acm竞赛算法——数据结构算法分类
  5. 怎么理解内存中的Buffer和Cache
  6. 破世界纪录了,用Python实现自动扫雷
  7. 侯捷C++八部曲笔记(五、内存管理)
  8. 使用lws编码的注意事项
  9. android五子棋盘的画法
  10. 随手小记之华硕笔记本安装系统