本篇是上一篇赫夫曼树构建与编码的后续,稍微详细讲一下解码的算法。

 

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

它对应的编码串即为:

java 赫夫曼编码 赫夫曼树译码_huffman树

 

下面是参考代码,注释我也打得比较详细了,算是边对着代码边讲解吧:

#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