《算法之美》一书中对哈夫曼编码(Huffman Code)的实现,值得学习的地方有很多。
哈夫曼编码为变长编码,与传统的ASCII的8bit等长编码不同,该方式对常出现的字符使用更少的编码来表示。
在大部分情况下其压缩率良好,但编码和还原都需要大量的CPU时间,且还原需要获得一份哈夫曼编码表,jpeg格式中便使用了哈夫曼编码进行压缩。
该方法不会产生一个编码是另外一个编码的前缀码的情况,也就是不会出现二义性。
其核心实现步骤为
1. 统计出符号频率
2. 将频率按大小排序
3. 每次选两个最小的值作为二叉树的两个叶子结点,小的位于左,大的位于右,它们的和作为它们的根结点,也就是构造一棵树,而后每次参与比较的为该”树”。
4. 重复直至只剩一个节点
5. 将3-4中形成的一颗二叉树左结点标0,右结点标1,从根到每个子结点途中遇到的结点序列组合起来就得到了哈夫曼编码。
#include <iostream>
#include <string>
#include <queue>
#include <map>
#include <iterator>
#include <algorithm>
using namespace std;
typedef vector<bool> HuffCode; //哈夫曼编码容器类型
typedef map<char, HuffCode> HuffCodeMap; //哈夫曼编码表类型
//结点基类(负责维护权值,在哈夫曼树中不论什么结点都存在权值,且后续处理依赖,非常适合作为基类)
class INode
{
public:
const int f; //权值
virtual ~INode() {}
protected:
INode(int f) : f(f) {}
};
//内部结点(或称为根结点,维护树的结构)
class InternalNode : public INode
{
public:
INode *const left; //左子结点
INode *const right; //右子结点
//构造函数将左右子结点的权值和作为该结点的权值
InternalNode(INode *c0, INode *c1) : INode(c0->f + c1->f), left(c0), right(c1) {}
~InternalNode() { delete left; delete right; }
};
//叶子结点(维护符号,于哈夫曼编码中只有叶子结点才存“符号”,也是通过这样来保证没有二义性)
class LeafNode : public INode
{
public:
const char c; //符号
LeafNode(int f, char c) :INode(f), c(c) {}
};
//一种在C++中对容器使用自定义比较函数的方式,称为函数对象(function object),这种方式因为容器类的声明方式具有较高的可读性和灵活性。
//另一种方式则是直接定义为函数cmp(T1, T2)的方式,称为函数指针(function pointer),并且声明容器时作为第一个参数如 QMap<T,K> map(cmp)
struct NodeCmp
{
bool operator() (const INode *lhs, const INode *rhs) const { return lhs->f > rhs->f; }
};
INode *BuildTree(map<char, int> freques)
{
/*
定义优先队列,这样将字符频率倒进来之后便已经排好序,其本质为堆(heap)
注意,该类为STL中的适配器(Adapter),仅实现算法,因此它需要提供容器以存放数据,不提供则默认为vector。
容器可以是自定的或是STL中提供的,但容器需要提供随机访问(Random Access)能力和至少以下接口:
empty()、size()、front()、push_back()、pop_back()。
标准容器中的vector和deque就能满足这些要求。
用堆的方式使总能按“顺序”取出元素,是一个非常巧妙且效率很高的做法,并且满足第3步的要求。
*/
priority_queue<INode *, vector<INode *>, NodeCmp> trees;
map<char, int>::iterator it = freques.begin();
//将字符和其频率建为叶结点放入堆中
for (; it != freques.end(); it++)
trees.push(new LeafNode(it->second, it->first));
while (trees.size() > 1)
{
/*注:此处书中有误,LR写反
该步骤即是不断的取出两个最小的结点组合成一个“树”,随后将其放回
注意,小的位于左,大的位于右,所以先左后右。
由于INode是LeafNode的基类,且在该步骤中不关注字符是什么,只按权值(频率)建树,而权值位于基类中。
InternalNode在此处同理。
巧妙的设计利用了c++的类继承特性在这里提供了非常好的可读性和便捷性。
*/
INode *childL = trees.top();
trees.pop();
INode *childR = trees.top();
trees.pop();
INode *parent = new InternalNode(childL, childR);
trees.push(parent);
}
/*由于循环后的最终结果必定是只剩一个InternalNode,也就是所有过程中的叶结点合并而成的子树都合为一棵树,所以返回即可*/
return trees.top();
}
//哈夫曼编码表生成函数,用的递归方式,也利用了C++继承的特性带来良好的可读性
void GenerateCodes(const INode *node, const HuffCode &prefix, HuffCodeMap& outCodes)
{
//如果递归到了叶子结点则证明已经发现了一个编码,将符号和它的编码存入哈夫曼编码表中,它的编码会通过递归左右子树分别填充得来。
//且巧妙的利用了dynamic_cast这个运行时转换的特性,规避了非叶子结点
//dynamic_cast提供了安全的向下转换,如果无法转换为LeafNode则会返回空指针(null),在这里则等于不执行任何操作
if (const LeafNode *lf = dynamic_cast<const LeafNode *>(node))
outCodes[lf->c] = prefix;
else if (const InternalNode *in = dynamic_cast<const InternalNode*>(node))
{
/*
如果不是叶子结点则看看是否内部结点,其安全性同上。
编码生成的方式也很巧妙,利用递归来迭代。
生成了左右两个子编码“前缀”,并且分别往最后填0和1,注意,每次都往最后填等于编码会随着递归层次不同而正确填充。
随后分别递归到左右子树来遍历整个哈夫曼树。
*/
HuffCode leftPrefix = prefix;
leftPrefix.push_back(false);
GenerateCodes(in->left, leftPrefix, outCodes);
HuffCode rightPrefix = prefix;
rightPrefix.push_back(true);
GenerateCodes(in->right, rightPrefix, outCodes);
}
}
int main(void)
{
map<char, int> freques;
cout << "Enter Text Data: " << endl;
string str;
getline(cin, str);
const char *ptr = str.c_str();
while (*ptr != '\0')
{
if (freques.find(*ptr) != freques.end())
freques[*ptr]++;
else
freques[*ptr] = 1;
ptr++;
}
INode *root = BuildTree(freques);
HuffCodeMap codes;
GenerateCodes(root, HuffCode(), codes);
delete root;
for (HuffCodeMap::const_iterator it = codes.begin(); it != codes.end(); it++)
{
cout << it->first << " ";
//copy输出到流,一个良好的方法。
copy(it->second.begin(), it->second.end(), ostream_iterator<bool>(cout));
cout << endl;
}
return 0;
}