其实这个霍夫曼编码本身不是一个很难的技巧(也是霍夫曼在期末考试的过程中想出来的方案:)),因为中间用到了贪心的思想,所以也在这里列举了出来。这个问题本身在计算机系的很多教材上都出现过。这里权且记录下来。
霍夫曼的编码是这样的。假设我有一组带压缩的文本,里面各个字符出现的频率不同,现在需要对他们进行压缩。比如
假设我们有100,000个字符的文本.最直观的压缩办法就是原来每个字符要8个bits。现在我一共只有6个字符,那我就把每个字符用3个二进制位来表示,这样所有100,000个字符用300,000个bit就可以表示了。这种是最直观的方案。但是霍夫曼提出的方案更精妙一些。他提出,基于每个字符出现的频率不同,可以让出现次数多的字符用更少的二进制位来描述,出现次数少的字符用多一些二进制来描述。比如上图显示的这个Variable-length codeword里面。a出现的频率最高,所以用一个二进制位0来表示。而f出现的频率很小,所以用4个二进制位来表示。这样总共 (45 · 1 + 13 · 3 + 12 · 3 + 16 · 3 + 9 · 4 + 5 · 4) · 1,000 = 224,000 bits。可以看到这个是比原来的方案更优化的解法。
我们一样的还是用一张图来描述霍夫曼编码的流程:
这个过程概括的说就是一个根据频率建立二叉树的过程。建完之后对应的编码也就完成了。
第一步a. 这个a就和之前的活动选择问题一样,把需要的所以字符按照频率排序。
第二部b. 选取出现频率最小的两个节点 f 和 e。组成一个新的节点,新的节点的频率就是e和f的和。原来的e和f分别成了新节点的左子节点和右子节点。(注意这里一个默认的规则就是频率小的是左子节点,大的是右子节点。)然后把之前的两个节点从原来的组中删除,加新的节点加入排序。
第三部c. 其实和第二部雷同,就是一个循环的过程。这里再次去除队列中的最小频率的两项(这时是c和b)。组成新的节点加入队列排序。
如此循环往复,最后就形成了(f)这个二叉树。现在有了二叉树只有,我们把左子树这条边标记为0,右子树标记为1。这样就差生了对应的编码方式 a=0; b=101;....
下面是对应的代码:
// Huffman_Coding.cpp : Defines the entry point for the console application. // #include "stdafx.h" /* The simplest construction algorithm uses a priority queue where the node with lowest probability is given highest priority: 1. Create a leaf node for each symbol and add it to the priority queue. 2. While there is more than one node in the queue: #1. Remove the node of highest priority (lowest probability) twice to get two nodes. #2. Create a new internal node with these two nodes as children and with probability equal to the sum of the two nodes' probabilities. #3. Add the new node to the queue. 3. The remaining node is the root node and the tree is complete. */ #include <queue> #include <map> #include <iostream> struct huffmanNode { huffmanNode* l; huffmanNode* r; char t; double f; huffmanNode() { l = NULL; r = NULL; t = 0; f = 0; } ~huffmanNode() { delete l; delete r; } }; struct huffmanNodeSort { bool operator()(huffmanNode* h1, huffmanNode* h2) { // make top the smallest return h1->f > h2->f ; } }; void generateResult (huffmanNode* pNode,std::map< char, std::vector<bool> >& output, std::vector<bool>& prefix) { if (pNode->l) { // if left, there must be right as well prefix.push_back(0); generateResult(pNode->l,output, prefix); prefix.back() = 1; generateResult(pNode->r, output, prefix); prefix.pop_back(); } else output[pNode->t] = prefix; } void huffmanCoding(const std::map<char, double>& m, std::map< char, std::vector<bool> >& output ) { std::priority_queue<huffmanNode*, std::vector<huffmanNode*>, huffmanNodeSort> pqueue; output.clear(); if (m.empty()) return; // init the queue std::map<char, double>::const_iterator mapIterator; for (mapIterator=m.begin(); mapIterator!=m.end();++mapIterator) { huffmanNode* pNode = new huffmanNode(); pNode->t = mapIterator->first; pNode->f = mapIterator->second; pqueue.push(pNode); } // create the tree huffmanNode* tree = NULL; while (!pqueue.empty()) { huffmanNode* top = pqueue.top(); pqueue.pop(); if (pqueue.empty()) { tree = top; } else { huffmanNode* top2 = pqueue.top(); pqueue.pop(); huffmanNode* pNew = new huffmanNode(); pNew->f = top->f+top2->f; pNew->l = top; pNew->r = top2; pqueue.push(pNew); } } // set to the result std::vector<bool> prefix; generateResult(tree, output, prefix); delete tree; } int _tmain(int argc, _TCHAR* argv[]) { std::map<char, double> freq; freq['a'] = 0.45; freq['b'] = 0.13; freq['c'] = 0.12; freq['d'] = 0.16; freq['e'] = 0.09; freq['f'] = 0.05; std::map< char, std::vector<bool> > s; huffmanCoding(freq, s); std::map<char, std::vector<bool> >::const_iterator si = s.begin(); for (;si!=s.end();++si) { std::vector<bool>::const_iterator it = si->second.begin(); std::cout<<si->first <<" = "; for (;it != si->second.end(); ++it) std::cout << *it ; std::cout<<std::endl; } system("pause"); return 0; }
写这个代码的时候挺感慨,之前看到过一个算法描述语言的帖子说到c语言比C++更适合。但是从我的角度说还是C++更亲切。这里用了priority_queue并不是说我不能自己写个二叉堆。只是说,我们在描述这个算法的时候可以以更多的精力关注这个算法本身,而不是从轮胎开始造汽车:)