Huffman编码
1. OJ题目描述
在通讯领域,经常需要将需要传送的文字转换成由二进制字符组成的字符串。在实际应用中,由于总是希望被传送的内容总长尽可能的短,如果对每个字符设计长度不等的编码,且让内容中出现次数较多的字符采用尽可能短的编码,则整个内容的总长便可以减少。另外,需要保证任何一个字符的编码都不是另一个字符的编码前缀,这种编码成为前缀编码。
而赫夫曼编码就是一种二进制前缀编码,其从叶子到根(自底向上)逆向求出每个字符的算法可以表示如下:
在本题中,读入n个字符所对应的权值,生成赫夫曼编码,并依次输出计算出的每一个赫夫曼编码。
输入
输入的第一行包含一个正整数n,表示共有n个字符需要编码。其中n不超过100。
第二行中有n个用空格隔开的正整数,分别表示n个字符的权值。
输出
共n行,每行一个字符串,表示对应字符的赫夫曼编码。
样例输入
8
5 29 7 8 14 23 3 11
样例输出
0110
10
1110
1111
110
00
0111
010
提示
赫夫曼树又名最优二叉树,它是一类带权路径长度最小的二叉树。通过构造赫夫曼树,我们可以得到赫夫曼编码,从而使得通信能够得到更高的效率。在本题中,构造赫夫曼树的过程使用了从叶子到根的逆向顺序,另外,还有一种从根出发直到叶子的赫夫曼编码构造算法,这将在下一题中进行讨论。
2.哈夫曼树的构造思路
一般Huffman树是将编码的结点所对应的权重或者频率按照从小到大的顺序自底而上构建,权重越大的结点距离根节点越近,编码长度越短。
编码的结点全部在叶子节点上,且向左全部编码为0,向右全部编码为1,最终结果为从根节点遍历路径的编码总和。
以本题为例(本题例子样例不好,左右结点左右次序略有不同,但是构造思想大同小异),先将八个结点的权值升序排列: 3,5,7,8,11,14,23,29
构建Huffman树的步骤为:
1. 选取两个权值最小的结点构建

2. 然后删掉序列中的3和5,并将他们的父节点加入到新序列中(有序插入)
7,8,8,11,14,23,29
再次选择他们当中最小的两个权值插入,如果有相同值,如第二和第三都是8,那么优先选择没有插入到Huffman树中的结点,即选择7,8构成新的结点。如果第三位不是8,那么就应该选择将7插入到第一步构成的8的左边

3. 重复第二步
序列更新为:8,11,14,15,23,29
4. 不断循环
更新为:14,15,23,29

5,最终生成的Huffman树为:

3.代码实现思路
-
因为本题的特殊性,针对头两个结点单独处理,即5,3排列的顺序。对最后的根节点也单独处理,即48,52汇总的根节点100
-
Huffman树的结点装在vector<Node*>中,方便结点的删除,插入和权值排序
-
其他结点插入方法为:
-
每次在集合中将删掉上次已经插入的两个结点,
-
插入他们构成的父节点
重新对集合排序 -
再取最小权重的结点,新建两个新选的结点的父亲结点,更新其左右子树,插入完成,进入下一次循环
-
-
编码方法:
对树遍历,查找编码的权重,并且注意判断编码的一定为叶结点,即匹配的结点必须没有子树
4.代码实现
#include<iostream>
#include <vector>
#include <algorithm>
using namespace std;
int flag=1;
typedef struct Node{
int w;
Node *left;
Node *right;
bool isAdd; //判断该结点是否已经插入Huffman树中
};
void init(Node **a,int n){
int w;
for(int i=0;i<n;i++){
a[i]->left=a[i]->right=NULL;
a[i]->isAdd= false;
cin>>w;
a[i]->w=w;
}
}
bool comp(Node *a,Node *b){
return (a->w<b->w);
}
/*
* 构建树
* 因为本题的特殊性,针对头两个结点单独处理,即5,3排列的顺序
* 对最后的根节点也单独处理
*/
Node* HuffmanTree(Node **a,int n){
vector<Node*> tree;
// push the Node from array "a" into vector "tree"
for(int i=0;i<n;i++)
tree.push_back(a[i]);
sort(tree.begin(),tree.end(),comp);
Node *firstIns=new Node;
firstIns->left=tree[1];
firstIns->right=tree[0];
firstIns->w=tree[1]->w+tree[0]->w;
firstIns->isAdd= true;
//start insert
//对第一对结点的父节点新建完毕,开始建树
/*
* 思路是每次在集合中将删掉上次已经插入的两个结点,
* 插入他们构成的父节点
* 重新对集合排序
* 再取最小权重的结点,插入树中,进入下一次循环
* */
Node *ParentNode=new Node;
for(int i=0;i<n-1;i++){
if(tree.size()==2) break;
//delete the front two element and insert their parent in the tree
tree.erase(tree.begin(),tree.begin()+2);
if(i==0)
tree.push_back(firstIns);
else
tree.push_back(ParentNode);
//对新的容器排序
sort(tree.begin(),tree.end(),comp);
//排完序后再取前两位插入哈夫曼树
Node *insNode=new Node;
//如果第二位与第三位相比结点已经插入树中,则优先选择未插入的进行插入
if(tree[1]->w==tree[2]->w){
if(tree[1]->isAdd== true){
Node *temp=tree[1];
tree[1]=tree[2];
tree[2]=temp;
}
}
//将未插入的结点往左边插入
if(tree[0]->isAdd==false){
insNode->left=tree[0];
insNode->right=tree[1];
}else{
insNode->left=tree[1];
insNode->right=tree[0];
}
insNode->w=tree[0]->w+tree[1]->w;
insNode->isAdd= true;
ParentNode=insNode;
}
//单独对根节点处理,原理同上
Node *rootNode=new Node;
if(tree[0]->w<tree[1]->w){
rootNode->left=tree[0];
rootNode->right=tree[1];
}else{
rootNode->left=tree[1];
rootNode->right=tree[0];
}
rootNode->w=tree[0]->w+tree[1]->w;
return rootNode;
}
//编码
void HuffmanCoding(Node *root,int elem,string &code){
if (root==NULL)
return ;
else {
if(root->w==elem&&root->left==NULL&&root->right==NULL){
//cout<<" 这是第"<<flag<<"次成功编码: ";flag++;
cout<<code<<endl;
return ;
}else{
string record=code;
code.append("0");
HuffmanCoding(root->left,elem,code);
code=record;
code.append("1");
HuffmanCoding(root->right,elem,code);
code=record;
}
}
}
//先序遍历
void PreOrderTraverse(Node* root) {
if (root) {
cout << root->w << " ";
PreOrderTraverse(root->left);
PreOrderTraverse(root->right);
}
}
int main(){
int n; cin>>n;
Node **a=new Node*[n];
for(int i=0;i<n;i++)
a[i]=new Node;
init(a,n);
Node *root=new Node;
root=HuffmanTree(a,n);
cout<<"创建的Huffman树先序遍历为:"<<endl;
PreOrderTraverse(root);
cout<<"\n\n\n";
for(int i=0;i<n;i++){
cout<<"权重为"<<a[i]->w<<"的编码--------- ";
string code;
HuffmanCoding(root,a[i]->w,code);
}
return 0;
}