本篇是上一篇赫夫曼树构建与编码的后续,稍微详细讲一下解码的算法。
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;
f) 继续循环,一直到编码串末尾;
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