赫夫曼(Huffman)树,由发明它的人物命名,又称最优树,是一类带权路径最短的二叉树,主要用于数据压缩传输。node
赫夫曼树的构造过程相对比较简单,要理解赫夫曼树,要先了解赫夫曼编码。算法
对一组出现频率不一样的字符进行01编码,若是设计等长的编码方法,不会出现混淆的方法,根据规定长度的编码进行翻译,有且只有一个字符与之对应。好比设计两位编码的方法,A,B,C,D字符能够用00-11来表示,接收方只要依次取两位编码进行翻译就能够得出原数据,但若是原数据只由n个A组成的,那发出的编码就是2n个0组成,这样的压缩效率并非十分理想(两位编码只是举例,实际上表示全部ASCII字符须要255个编码,则须要使用八位编码方式,这样若是原数据字符重复率较高的话,效果天然不理想)。若是设计的编码分别为:0,00,1和01,这样我发送了n个A组成的数据,这样一来只须要发送n个0,虽然会节省不少的数据传输量,但若是我传送了“0000”,则有多种翻译方法,能够翻译为“AAAA”,“ABB”,也不是可行的解决方案。若是有一种设计方式,用较短的编码表示出现频率较高的字符,用相对较长的编码表示出现频率相对较高的字符,而每一个字符的解码不产生混淆,效果将会十分理想,而赫夫曼编码就是知足这种要求的编码方式,这种编码方式也叫前缀编码。数据结构
以一颗二叉树做为基础,规定左分支位0,右分支为1,以每一个出现的字符做为各叶子节点的权值构造一颗赫夫曼树。在一颗赫夫曼树之中,从根节点出发,每通过一个节点(经过左子树或者右子树),解码出来的字符也随之一步步肯定,由于从一个节点开始,经过左子树和右子树的结果是互斥的。并发
如左图所示,若是一开始从根节点开始,往右子树探索,则结果不可能会出现AB两种可能,再接下去往左子树探索就会获得C,往右子树就会获得D,保证在获得某一个字符以前的前缀编码不会是另外一个字符的编码。回到前一段,若是咱们把权值(出现频率)较高的节点放在深度较低的叶子节点上,权值较低的节点由于构造二叉树须要放的相对深一些,则可构造一颗赫夫曼数,具体方法以下:函数
1)根据给定的n个权值{w1,w2,...,wn}构成n棵二叉树的集合F = {T1,T2,...,Tn},其中每棵二叉树Ti中只有一个带权位wi的根节点,其中左右子树均为空。工具
2)在F中选取两颗根节点的权值最小的树做为左右子树构造一棵新的二叉树,且置新的二叉树的根节点的权值为其左,右子树上根节点的权值之和。优化
3)在F中删除这两棵树,同时将新的二叉树加入F中ui
4)重复(2)和(3)的过程,直到F只含一棵树为止。这棵树即是赫夫曼树。编码
文字描述可能有点费解,如下用一些示意图表示,整个过程可表示为:加密
整个过程下来可以保证权值较小的叶子节点比较先被选择到,从而保证一套编码下来权值较大的叶子节点编码较短(由于深度比较低),而权值较小的叶子节点编码相对就长一点。由于权值小的字符出现几率低,因此平均下来这种编码方式可以达到较高的压缩率。
接下来是算法代码的实现,通过以上的理论,咱们要完成的任务有:
1)用一个初始的字符串去构建一棵赫夫曼树(统计每一个字符的出现次数做为权值),前提是这个字符串中字符出现的几率必须和数据交换时候字符出现的几率大体匹配。
2)完成赫夫曼树的构建(经常使用的字符叶子结点深度较浅,不经常使用的叶子结点深度较深),并求出每一个字符对应的编码,存在一个Map数据结构中,这个Map暂称编码表
3)A要把一段数据传输给B,A对B说,我给你一棵赫夫曼树,到时候你就用这棵树对我发给你的内容进行解码,而后根据编码表求出原数据的编码并发给B
4)B根据收到的编码以赫夫曼树为基础进行解码,最后得出原数据内容
首先是树节点的数据结构:
class Node{ public: Type val; int weight; Node *parent,*left,*right; Node(Type v,int w):val(v),weight(w),parent(NULL),left(NULL),right(NULL){} Node(int w):weight(w),parent(NULL),left(NULL),right(NULL){} };
val为初始化字符的每一个字符,weight,权值,即出现次数,left和right分别指向节点的左右子树,parent指向父节点。
构建赫夫曼树的核心代码为:
void builTree(){ int n = nodes.size()*2-1; for(int i = nodes.size();i<n;i++){ vector<int> ret = selMin2(nodes); nodes.push_back(new Node(nodes[ret[0]]->weight+nodes[ret[1]]->weight)); nodes[ret[0]]->parent = nodes.back(); nodes[ret[1]]->parent = nodes.back(); nodes.back()->left = nodes[ret[0]]; nodes.back()->right = nodes[ret[1]]; } root = nodes.back(); }
初始字符串通过统计以后存在nodes容器中,定义为 vector<Node*> nodes ,由于开始构建的时候nodes存储的全为叶子节点,因此预先求出整棵赫夫曼树总的节点数为 int n = nodes.size()*2-1; (一棵彻底二叉树的总节点个数为全部叶子节点个数 x 2 - 1)其中selMin2()函数求出权值最小的且没有父节点的两个节点,返回这两个节点的下表,我本身写的复杂度为O(n),没有特别的优化过程。
完成赫夫曼树的构建后,从根节点开始求出每一个叶子节点(字符)的编码:
void getcode(){ //encoding all the character recursively if(root != NULL) Recur(root,""); } void Recur(Node *node,string c){ if(!node->left && !node->right){ //leaf node code[node->val] = c; } else{ Recur(node->left,c+'0'); Recur(node->right,c+'1'); } }
对应的编码和解码过程代码以下:
string encoding(string plain){ string res; for(auto elem : plain) res+=code[elem]; return res; }
string decoding(string enci){ string res; if(NULL != root){ int i= 0; Node *n = root; while(i<enci.size()){ if(enci[i] == '0') n = n->left; else n = n->right; if(n && !n->left && !n->right){ res.push_back(n->val); n = root; } i++; } } return res; }
编码过程当中的code为unordered_map类型,即为前文所提到的编码表。
一开始用”1111111111222222222333333334444444555555666667777888990”做为初始化字符串来构建赫夫曼树,写两个工具分别用于编码和解码,效果以下图所示:
此处只是一个模拟过程,因此编码结果我在代码中只是用字符串表示,实际上在传输过程当中是用bit表示(本来8个字符32位如今只须要22位bit),因此个人代码省略了A把编码转为bit表示的编码,而后B根据编码信息求出01字符串。
以上为本人对赫夫曼编码压缩(我的认为还能起到加密做用,此时赫夫曼树做为密钥)的拙见,文中的错误和不妥之处欢迎你们指出斧正。
尊重知识产权,转载引用请通知做者并注明出处!