最近感觉自己课业有所懈怠啊,稍微放下数据结构去搞别的方向的学习,结果专业课就超过我写的博客的进度了,简直恐怖.一定要好好反思一下了,那么今天就写一点,哈夫曼树和哈夫曼编码的相关知识吧
首先介绍一下编码格式: Huffman编码是一种前缀编码,即,任意字符的编码都不是其余字符编码的前缀串.如果我们对a,b,c,d分别编码为0,1,10,11,则它是不等长编码,但不是前缀编码,如果编码为00,01,10,11的话,此编码就是前缀编码但它是等长的,但是我们希望编码字符尽可能地短,于是可以将第一种编码的不等长特性和第二种编码的前缀编码特性结合到一起,就构成了Huffman编码--一种不等长的前缀编码
Huffman编码的思想: 以每个字符在字符串中出现的概率作为叶子的权,构造Huffman树,并对其进行某种方式遍历即得哈夫曼编码
什么是Huffman树? --WPL最小的树,即带权路径最小的二叉树,又称为最优二叉树,所谓带权,就是对一棵叶子权值确定的树,确定叶子的位置,使得所有叶子的带权路径长度之和最小.
什么是树的带权路径长度? --树中所有叶子的带权路径长度之和
那么什么是节点的带权路径长度? --从根节点出发,到该节点的路径长度乘以该节点权值所得的值
那么问题就说清楚了,没看懂不要紧,下面有例子说明
我们如何构造Huffman树呢? --Huffman树是经过构造得来的,形态不唯一但是带权路径长度一定最小,算法如下:
1.将所有带权叶子看作独立的二叉树,放在一个集合里构成森林;
2.选择两棵根节点权值最小的二叉树,合并为一棵新的二叉树,其根的权值为原来两棵树的根权值之和;
3.从森林中删去选中的两棵二叉树,并将合并的二叉树加入森林;
4.重复2~3步,直到森林中只剩一棵二叉树,即为所求的Huffman树
下面我们通过一个实例来生成Huffman树,假设叶子为7,5,2,4,6,9:
是不是非常简单?每次重复查找-合并即可,这让我想起了并查集,只不过并查集是越合并树深度越小,Huffman树越合并深度越大,Huffman树之所以不做路径压缩是因为Huffman编码时需要用到完整的Huffman树呢,下面我们看看编码原理:
1.对于每一个叶子,在Huffman树中找一条根节点到叶子的路径;
2.对于这条路径经过的中间节点,若为左子树,则路径标为'0',否则路径标为'0';
3.对于所有的叶子进行1~2操作即可;
比如对于我们的这一棵Huffman树来说,编码如下:
则对于这些叶子来说,权值为6的叶子编码为"00",权值为7的叶子编码为"01",权值为9的叶子编码为"10",权值为5的叶子编码为"110",权值为2的叶子编码为"1110",权值为4的叶子编码为"1111"
考虑如何实现Huffman树:
根据二叉树的性质,Huffman树没有度为1的节点,设其度为0的节点有n0个(即叶子数),度为2的节点有n2个,则:
n0 = n2+1
N(总节点数) = n0+n2 = 2*n0-1;
因此有n个叶子的Huffman树共有2*n-1个节点,从存储结构来看,我们直接选用数组即可(双亲数组存储法),当然二叉链表也是可以的,但是由于知道节点个数,无疑双亲数组是最方便的结构了
我们来安排一下节点结构,如下:
typedef struct HTNode //哈夫曼树节点;
{
char data; //节点的值;
int weight; //节点权重;
int parent,lc,rc; //节点之间的关系;
}HTree;
//节点之间的关系有:双亲节点、左子树、右子树三种;
对于n个叶子的Huffman树来说,我们开设一个2*n-1大小的数组即可:
int n;
HTree T[2*n-1];
这里我们可以巧妙地设定一下: T[2*n-1]中,前面n个节点用来存叶子,后面的节点用来存生成的节点,使用的时候记得初始化哦:
for(int i = 0; i < 2*n-1; i++) //初始化节点数组;
{
T[i].data = '@'; //随意设定
T[i].weight = 0;
T[i].parent = T[i].lc = T[i].rc = -1;
}
for(int i = 0; i < n; i++) //读入叶子;
cin>>T[i].data>>T[i].weight;
那么我们只要每次查找两个权值最小的节点,将其合并并修改其父节点和它们的对应关系即可,如何一次遍历查找两个最小值?
这里我们看一看@阿爽提供的思路:
void select(HTree T[],int &fmin,int &smin,int stp) //查找两个最小的节点,stp代表结束位置;
{
int min_1 = inf,min_2 = inf; //min_1,min_2用来暂存两个最小值;
for(int i = 0; i < stp; i++) //fmin,smin用来标记两个最小值的下标;
{
if(T[i].parent == -1)
{
if(T[i].weight < min_1)
{
smin = fmin;
min_2 = min_1;
fmin = i;
min_1 = T[i].weight;
}
else if(T[i].weight < min_2)
{
smin = i;
min_2 = T[i].weight;
}
}
}
}
把这两个算法串起来,Huffman树就生成了:
void HuffmanTree(HTree T[],int n) //生成Huffman树,共n个叶子节点;
{
for(int i = 0; i < 2*n-1; i++) //初始化节点数组;
{
T[i].data = '@';
T[i].weight = 0;
T[i].parent = T[i].lc = T[i].rc = -1;
}
cout<<"Enter "<<n<<" leaf(-ves), with data and value:"<<endl;
for(int i = 0; i < n; i++) //读入叶子;
cin>>T[i].data>>T[i].weight;
for(int i = 0; i < n-1; i++) //生成另外n-1个节点;
{
int fmin,smin;
select(T,fmin,smin,n+i);
T[n+i].lc = fmin,T[n+i].rc = smin;
T[fmin].parent = T[smin].parent = n+i;
T[n+i].weight = T[fmin].weight + T[smin].weight;
}
}
细心的读者可能会发现,每次传递进select函数里的结束位置都跟i有关,确切的说是n+i,这是为什么呢?
因为每选两个节点,都生成了一棵二叉树,T[2*n-1]中存放有效数据的位置就往后移了一位,选择在n+i结束,可以避免对数组中无效的位置进行查找,不但节省时间而且还能避免一些问题(比如无效节点权值小于有效节点权值从而被误选的问题),当i == n-2时,说明生成了n-1个新节点,Huffman树建立完成(数组有效数据下标从0开始)
那么我们如何建立Huffman编码呢?当然是拿着建立好的Huffman树去建立了,对每一个叶子,找一条从根到它的路径,经过左子树填'0',右子树填'1'的方式生成该叶子的编码,还记得吗?但是我们会遇到一个问题--我们只能从叶子往上找到根,不能轻易地从根找到叶子(双亲数组).解决: 我们可以拿着叶子找到根,记录路径,然后将路径反转,不就是根到叶子了吗?
提供两个思路: 1.使用栈记录路径,将栈清空的时候再保存路径即可;2.用字符串暂存路径,然后反着赋值给保存的空间即可;本文暂且采用第二个策略,Huffman编码的算法如下:
void encoding(HTree T[],HTCode C[],int n) //Huffman编码;
{
for(int i = 0; i < n; i++) //Huffman编码;
{
int t = i; //t用于保存上一个节点信息;
string tmp = "";
C[i].code = "";
for(int j = T[i].parent; j != -1; j = T[j].parent)
{
if(T[j].lc == t) tmp += '0';
else tmp += '1';
t = j;
}
for(int j = tmp.size()-1; j >= 0; j--) //字符串反转;
C[i].code += tmp[j];
}
}
有了Huffman编码,我们相当于有了一个字符-编码映射表,通过这样的映射关系可以给一段01串进行译码/判错的功能.毕竟,编码的目的是为了译码,不是么?假设我们有一个01串code ,我们可以对照码表进行比对,从而将01串分成若干段,如果无法分成整数段,则说明code的长度有问题或者说code是一个错误的编码(至少在当前的Huffman码下无法译码),若能分成若干段,就可以知道01串代指的是什么字符信息了(译码成功),算法实现如下:
bool decoding(string code,HTree T[],int n,string &decode) //Huffman译码;
{ //code是密码串,decode是译码串
int root,cur;
decode = "";
for(int i = 0; i < 2*n-1; i++) //寻找根节点;
if(T[i].parent == -1)
{
root = i;
cur = root;
break;
}
for(int i = 0; i < code.size(); i++)
{
if(T[cur].lc == -1 && T[cur].rc == -1)
{
decode += T[cur].data;
cur = root;
}
if(code[i] == '0')
cur = T[cur].lc;
else
cur = T[cur].rc;
if(i == code.size()-1) //处理最后一个节点;
{
if(T[cur].lc == -1 && T[cur].rc == -1)
{
decode += T[cur].data;
return 1;
}
return 0;
}
}
}
总之Huffman编码就是->建树->编码->译码的过程,实现也很简单,注意一些技巧即可,接下来我就把完整实现一遍Huffman:
#include<iostream>
#define inf 0x3f3f3f3f
#define max_leaf 100
#define max_node 199
using namespace std;
typedef struct HTNode //哈夫曼树节点;
{
char data; //节点的值;
int weight; //节点权重;
int parent,lc,rc; //节点之间的关系;
}HTree;
typedef struct HTCode //哈夫曼编码节点;
{
string code;
}HTCode;
void select(HTree T[],int &fmin,int &smin,int stp) //查找两个最小的节点,stp代表结束位置;
{
int min_1 = inf,min_2 = inf;
for(int i = 0; i < stp; i++)
{
if(T[i].parent == -1)
{
if(T[i].weight < min_1)
{
smin = fmin;
min_2 = min_1;
fmin = i;
min_1 = T[i].weight;
}
else if(T[i].weight < min_2)
{
smin = i;
min_2 = T[i].weight;
}
}
}
}
void HuffmanTree(HTree T[],int n) //生成Huffman树,共n个叶子节点;
{
for(int i = 0; i < 2*n-1; i++) //初始化节点数组;
{
T[i].data = '@';
T[i].weight = 0;
T[i].parent = T[i].lc = T[i].rc = -1;
}
cout<<"Enter "<<n<<" leaf(-ves), with data and value:"<<endl;
for(int i = 0; i < n; i++) //读入叶子;
cin>>T[i].data>>T[i].weight;
for(int i = 0; i < n-1; i++) //生成另外n-1个节点;
{
int fmin,smin;
select(T,fmin,smin,n+i);
T[n+i].lc = fmin,T[n+i].rc = smin;
T[fmin].parent = T[smin].parent = n+i;
T[n+i].weight = T[fmin].weight + T[smin].weight;
}
}
/*输出Huffman节点信息,用于检验哈夫曼树的正确性;
void out_Huffman(HTree T[],int n)
{
for(int i = 0; i < 2*n-1; i++)
cout<<T[i].data<<" "<<T[i].weight<<" "<<T[i].parent<<endl;
}
*/
void encoding(HTree T[],HTCode C[],int n) //Huffman编码;
{
for(int i = 0; i < n; i++) //Huffman编码;
{
int t = i; //t用于保存上一个节点信息;
string tmp = "";
C[i].code = "";
for(int j = T[i].parent; j != -1; j = T[j].parent)
{
if(T[j].lc == t) tmp += '0';
else tmp += '1';
t = j;
}
for(int j = tmp.size()-1; j >= 0; j--) //字符串反转;
C[i].code += tmp[j];
}
}
bool decoding(string code,HTree T[],int n,string &decode) //Huffman译码;
{
int root,cur;
decode = "";
for(int i = 0; i < 2*n-1; i++) //寻找根节点;
if(T[i].parent == -1)
{
root = i;
cur = root;
break;
}
for(int i = 0; i < code.size(); i++)
{
if(T[cur].lc == -1 && T[cur].rc == -1)
{
decode += T[cur].data;
cur = root;
}
if(code[i] == '0')
cur = T[cur].lc;
else
cur = T[cur].rc;
if(i == code.size()-1) //处理最后一个节点;
{
if(T[cur].lc == -1 && T[cur].rc == -1)
{
decode += T[cur].data;
return 1;
}
return 0;
}
}
}
int main()
{
int n,num; //n代表叶子数;
HTree T[max_node];
HTCode C[max_leaf];
string code,decode;
cout<<"Enter a num:"<<endl;
while(cin>>n)
{
HuffmanTree(T,n);
//out_Huffman(T,n);
encoding(T,C,n);
cout<<"Code Table: "<<endl;
for(int i = 0; i < n; i++)
cout<<T[i].data<<" "<<C[i].code<<endl;
cout<<"Enter a num: "<<endl;
cin>>num;
cout<<"Enter "<<num<<" group(s) of code:"<<endl;
while(num--)
{
cin>>code;
if(decoding(code,T,n,decode))
cout<<"The decoded sequence: "<<decode<<endl;
else cout<<"The code's wrong!"<<endl;
}
cout<<"Enter a num:"<<endl;
}
return 0;
}
注意: 其实在程序中,encoding(编码)的过程不是必须的,我们译码也用不到HTCode[]这个数组,而是直接在Huffman树里找.只不过有了这个数组我们可以直观地检视字符-编码映射表,从而快速查错,查谁的错?当然是查译码模块的错啦,如果译码模块(decoding函数)写错了,则译码以后的结果将会和HTCode[]里的字符不匹配,当然如果你要是连编码模块(encodeing函数)都写错了,那我也没办法了~~~
贴一张代码运行效果:
写这个的时候突然想到并查集,不多说了,写博客去...