本篇是上一篇赫夫曼树构建与编码的后续,稍微详细讲一下解码的算法。
Huffman解码算法流程:
1.定义指针p指向赫夫曼树结点,实际是记录结点数组的下标;
2.定义指针i指向编码串,定义ch逐个取编码串的字符;
3.初始化:读入编码串,设置p指向根结点,i为0;
4.执行以下循环:
a)取编码串的第i个字符放入ch;
b)如果ch是字符0,表示往左孩子移动,则p跳转到右孩子;
c)如果ch是字符1,表示往右孩子移动,则p跳转到右孩子;
d)如果ch非0非1,表示编码串有错误,输出error表示解码失败;
e)检查p指向的结点是否为叶子;
i.如果是叶子,输出解码,p跳回根节点;
ii.如果不是叶子,设置ch为3;
继续循环,一直到编码串末尾;
5.循环执行完后,如果ch值为3,输出解码失败,否则成功结束。
解码关键:赫夫曼编码是一种前缀编码,解码时不会因为编码串混肴;
例如:
字符串AACACBDEDDDD 编码后我们可以获得对应的前缀:
A: 10;B: 1110;C: 110;D: 0;E: 1111
它对应的编码串即为:
下面是参考代码,注释我也打得比较详细了,算是边对着代码边讲解吧:
#include <iostream>
#include <string>
#include <cstring>
using namespace std;
#define error -1
#define ok 1
const int MaxW = 9999; //假设结点权值不超过9999
//定义huffman树结点类
class HuffNode
{
public:
int weight; //权值
int parent; //父结点下标
int leftchild; //左孩子下标
int rightchild; //右孩子下标
char data; //解码数据
};
//定义huffman树类
class HuffMan
{
private:
void MakeTree(); //建树,私有函数,被公有函数调用
void SelectMin(int pos, int *s1, int *s2);
//从1到pos的位置找出权值最小的两个结点,结点下标存在s1和s2中
public:
int len; //结点数量
int lnum; //叶子数量
HuffNode *huffTree; //huffman树,用数组表示
string * huffCode; //每个字符对应的huffman编码
void MakeTree(int n,int wt[],char data[]); //公有函数,被主函数main调用
void Coding(); //公有函数,被主函数main调用
void Destroy();
int Decode(const string codestr,char txtstr[]);
};
//构建huffman树
void HuffMan::MakeTree(int n,int wt[],char data[]) //参数是叶子结点数量和叶子权值
{//公有函数,对外接口
int i;
lnum = n;
len = 2*n-1;
huffTree = new HuffNode[2*n];
huffCode = new string [lnum+1]; //位置从1开始计算
//huffCode实质是个二维字符数组,第i行表示第i个字符对应的编码
//huffman树huffTree初始化
for(i=1;i<=len;i++)
{
huffTree[i].weight = wt[i-1]; //第0号不用,从1开始编号
huffTree[i].data=data[i-1]; //解码用
}
for(i=1;i<=len;i++)
{
if(i>n) huffTree[i].weight=0; //前n个结点是叶子,已经设值
huffTree[i].parent = 0;
huffTree[i].leftchild = 0;
huffTree[i].rightchild=0;
}
MakeTree(); //调用私有函数建树
}
void HuffMan::SelectMin(int pos,int *s1,int *s2)
//找出最小的两个权值的下标
//函数采用地址传递的方法,找出的两个下标保存在s1和s2中
{
int w1,w2,i;
w1=w2=MaxW; //初始化w1和w2为最大值,在比较中会被实际的权值替换
*s1=*s2=0;
for(i=1;i<=pos;i++) //对结点数组进行搜索
{
if(huffTree[i].weight < w1 && huffTree[i].parent == 0)
{ //如果i结点的权值小于w1,且第i结点是未选择的结点(父亲为0)
w2 = w1; //把w1,s1保存到w2,s2,即原来的第一最小值变成第二最小值(不理解?)
*s2 = *s1;
w1 = huffTree[i].weight; //把i结点的权值和下标保存到w1,s1,作为第一最小值
*s1=i;
}
else if(huffTree[i].weight < w2 && huffTree[i].parent == 0)
{
w2=huffTree[i].weight;*s2=i;
}
//否则如果i结点的权值小于w2,且i结点是未选中的,把i结点的权值和下标保存到w2和s2,作为第二最小值
}
}
void HuffMan::MakeTree()
{//私有函数,被公有函数调用
int i,s1,s2;
//构造huffman树HuffTree的n-1个非叶子结点
for(i=lnum+1;i<=len;i++)
{
SelectMin(i-1,&s1,&s2); //找出两个最小权值的下标放入s1和s2中
for(i = lnum+1;i <= len;i++)
{
SelectMin(i-1,&s1,&s2); //找出两个最小权值的下标放入s1,s1
huffTree[s1].parent = i; //将找出的两棵权值最小的字数合并为一颗子树
huffTree[s2].parent = i; //结点s1,s2的父亲设为i
huffTree[i].leftchild = s1; //i的左右孩子为s1,s2
huffTree[i].rightchild = s2;
huffTree[i].weight = huffTree[s1].weight + huffTree[s2].weight; //i的权值等于s1,s2权值和
}
}
}
//销毁huffman树
void HuffMan::Destroy()
{
len = 0;
lnum = 0;
delete []huffTree;
delete []huffCode;
}
//huffman编码
void HuffMan::Coding()
{
char *cd;
int i,c,f,start;
//求n个叶结点的huffman编码
cd = new char[lnum]; //分配求编码的工作空间
cd[lnum-1] = '\0';
for (i=1;i<=lnum;++i) //逐个字符求huffman编码
{
start = lnum-1; //编码结束符位置
//书P147第二个for有参考
for(c = i,f = huffTree[i].parent; f != 0; c = f, f = huffTree[f].parent) //从叶子到根逆向球编码
if(huffTree[f].leftchild == c)
{
cd[--start] = '0';
}
else
{
cd[--start] = '1';
}
huffCode[i] = new char[lnum-start]; //为第i各字符编码分配空间
huffCode[i].assign(&cd[start]); //把cd中从start到末尾的编码复制到huffCode中
}
delete []cd; //释放工作空间
}
//huffman解码
int HuffMan::Decode(const string codestr,char txtstr[])
{
//编码串是codestr,解码结果放在txtstr中
//编码0表示往左孩子移动,编码1表示往右孩子移动
int i,k,c;
char ch;
c = len; //c表示结点数组的下标,根节点是结点数组的最后一个结点,所以c一开始指向根节点
k = 0; //tstr的指针
for(i = 0;i < codestr.length();i++)
{
ch = codestr[i]; //取编码串的第i个字符放入变量ch
if(ch == '0') //如果ch是字符0,向左孩子移动,c跳转到左孩子
{
c = huffTree[c].leftchild;
}
else if(ch == '1') //同理
{
c = huffTree[c].rightchild;
}
else //解码失败
{
return error;
}
if(huffTree[c].leftchild == 0 && huffTree[c].rightchild == 0)
{//c指向结点是否叶子,解码串txtstr的第k保存结点c内字符,c跳回根结点
txtstr[k] = huffTree[c].data;
k++;
c = len;
}
else
{
ch ='\0'; //用于检查不完整编码的报错
}
}
if(ch == '\0') return error;
else txtstr[k] = '\0'; //解码成功,加入字符串结束符
return ok;
}
//主函数
int main()
{
int t,n,i,j,k;
int wt[800];
HuffMan myHuff;
string codestr;
char txtstr[800];
char data[800];
cout<<"请输入解码实例的个数,按回车结束:"<<endl;
cin>> t;
for(i=0;i<t;i++)
{
cout<<"请输入上一步赫夫曼编码第"<<i+1<<"个实例所得权值个数,按回车结束:"<<endl;
cin>>n;
cout<<"请输入上一步赫夫曼编码第"<<i+1<<"个实例所得各个权值(空格间隔),按回车结束后在下一行输入对应的字符,按回车结束:"<<endl;
for(j=0;j<n;j++)
cin>> wt[j];
for(j=0;j<n;j++)
cin>>data[j];
myHuff.MakeTree(n,wt,data);
myHuff.Coding();
cout<<"请输入需要解码的编码串个数,按回车结束:"<<endl;
cin>> k;
for (j=1;j<=k;j++)
{
cout<<"请输入需要解码的第"<<j<<"个编码串,按回车结束:"<<endl;
cin>>codestr;
if(myHuff.Decode(codestr,txtstr) == 1)
{
cout<<"解码结果:"<<txtstr<<endl;
}
else cout<<"Error!您输入的编码有误,请重新运行本程序并输入正确编码。"<<endl;
}
myHuff.Destroy();
}
return 0;
}
以下是我在Huffman编码解码过程中的关键点,总结如下:
1.编码从叶子开始,解码则从根结点开始;
2.数组下标最开始指向赫夫曼树根结点,最终结束于叶子;
3.根据编码表,编码串0为左孩子,1为右孩子,根据左右孩子跳到相应结点,左右孩子为0则解码完成;
4.如果编码串扫描完后停在非叶子结点(左右孩子均为0的中间结点),则解码失败需要报错;
作者:Nathaneko