[关闭]
@rg070836rg 2015-09-04T19:17:49.000000Z 字数 7060 阅读 2828

哈夫曼树与哈夫曼编码

data_structure

一、概念

哈夫曼树(Huffman tree),又名最优树,指给定n个权值作为n的叶子结点,构造一棵二叉树,若带权路径长度达到最小,称这样的二叉树为最优二叉树,也称为哈夫曼树(Huffman tree)。

二、想法

2.1、节点结构

哈夫曼树的节点中应该存在数据,权重,双亲以及左右孩子,
所以节点构造如下:

  1. template <class T>
  2. struct HuffmanNode
  3. {
  4. T data; //数据
  5. double weight; //符号出现的频率
  6. int parent; //双亲结点的坐标
  7. int lchild; //左孩子的坐标
  8. int rchild; //右孩子的坐标
  9. };

2.2、哈夫曼树结构及树的构造方法

我们先说,如何去构造一个哈夫曼树,应该是根据权值的不同,每次找到两个小值,把他们两个拼凑起来,其中,权值应该是通过统计得到的,后面会提到方法,这边暂且假设已经得到。

  1. template<class T>
  2. class HuffmanTree
  3. {
  4. ……………………
  5. private:
  6. vector<HuffmanNode<T> > huffmantree; //huffmantree所有节点的存储空间
  7. int n; //叶子结点数
  8. };

哈夫曼树,接受一个存有所有叶子节点的向量,根据这些向量,来构造一棵树,我们知道,n个叶子节点,最后会生成2n-1个节点,所以需要先resize树向量。然后把huffmantree里面初值赋完,接下来,我们要找两个权值最小和次小的节点,并用这两个节点信息生成新的节点,填入。

  1. template<class T>
  2. HuffmanTree<T>::HuffmanTree(vector<HuffmanNode<T> > &leafs)
  3. {
  4. n = leafs.size(); //叶子节点个数赋值给n;
  5. huffmantree.resize(2*n-1); //为为分支结点预留向量空间
  6. for (int i=0;i<n;i++)
  7. {
  8. huffmantree[i].data=leafs[i].data;
  9. huffmantree[i].weight=leafs[i].weight;
  10. huffmantree[i].parent=huffmantree[i].lchild=huffmantree[i].rchild=-1;
  11. }
  12. int least, less; //最小数 次小数的下标
  13. for( i=n; i<2*n-1; i++)
  14. {
  15. SelectSmall(least, less, i); //找到最小值 次小值的结点下标
  16. //由之前的两个最小值生成新结点,
  17. huffmantree[least].parent = huffmantree[less].parent = i;//原2个节点的父节点置值
  18. huffmantree[i].data = i;//并没什么作用。。后面输出也会被过滤
  19. huffmantree[i].parent = -1;
  20. huffmantree[i].lchild = least;
  21. huffmantree[i].rchild = less;
  22. huffmantree[i].weight = huffmantree[least].weight + huffmantree[less].weight;
  23. }
  24. }

2.3、找最小值以及次小值

下面介绍一下,如何找到最小值和次小值:
首先,只有没有被用过的节点才有比较的资格,也就是父节点为-1的才能参与比较,每轮比较,我们从头开始循环.循环判断过程如下


  • 父节点是否为-1


    • 当当前值比最小值小


      • 那么先把原先的最小值抛给最小值,并保存次小值序号,
      • 更新最小值及序号。
    • 当前值大于等于最小值,且小于次小值

      • 更新次小值及其序号
  1. template<class T>
  2. void HuffmanTree<T>::SelectSmall(int &least,int &less,int i)
  3. {
  4. least = less = 0;
  5. int min1 = INT_MAX;
  6. int min2 = INT_MAX;
  7. for(int j=0; j<i; j++)
  8. {
  9. if(huffmantree[j].parent == -1) //当没有父节点才有资格比较
  10. {//筛选没有父结点的最小值和次小值
  11. if(huffmantree[j].weight<min1)
  12. {//如果比最小值小
  13. min2 = min1; //把原来的最小值先抛给次小
  14. less = least; //保存次小值的序号
  15. min1 = huffmantree[j].weight;//保存最小值
  16. least = j; //能保存最小值序号
  17. }
  18. else if((huffmantree[j].weight>=min1)&&(huffmantree[j].weight<min2))
  19. {//如果大于等于最小值,且小于次小值
  20. min2 = huffmantree[j].weight;
  21. less = j;
  22. }
  23. }
  24. }
  25. }

2.4、频率统计

上面的构造函数,是根据所给的含有频率信息的HuffmanNode数组构造数,下面,我来介绍一下,对于ASCII码的字符(非汉字)如何统计。
思路如下,因为ASCII码值,不超过256个,所以,可以建立一个大小为256的int数组,初值置为0,遍历字符串,遇到一个,对应值加1,可以达到简单的统计效果,键值就是0~256,内容就是出现的次数。

  1. int frequency[256];//用于记录每个ASCII出现的次数
  2. memset(frequency,0,sizeof(frequency));
  3. for (int i = 0; i < rst.length(); i++)
  4. frequency[rst[i]]++; //每出现一次,次数加1
  5. for ( i = 0; i < 256; i++)
  6. {
  7. if (frequency[i] != 0)
  8. {
  9. weight.push_back(frequency[i]*1.0/rst.length()*100); //当频率不为0的时候,压入权重向量//这个做了个规约处理
  10. s.push_back(i); //同时把字符对应的ASCII值存到S向量
  11. }
  12. }
  13. for ( i = 0; i < s.size(); i++) //把组织好的值压入向量中,供构造树。
  14. {
  15. HuffmanNode<char> tmp={ s[i], weight[i], -1, -1, -1 };
  16. huffnode.push_back(tmp);
  17. }
  18. HuffmanNode<char> *hn=new HuffmanNode<char> [s.size()];
  19. for (i=0;i<s.size();i++)
  20. {
  21. hn[i]=huffnode[i];
  22. }

这个统计字频的思路很简单,但是对于中文是完全无效的,算是缺陷。这边在频率统计完了之后,顺便构造了Node数组,便于后面构造树。

2.5、生成编码字符串

首先,我们要对这棵哈夫曼树的叶子节点编码,得到每个叶子节点的码值,然后在读取源码,生成对应的01编码。
编码的原理,就是从当前节点向前回溯,如是其父节点的左孩子插入1,相反插0;知道根节点停止。

  1. template<class T>
  2. vector<int> HuffmanTree<T>::GetCode(int i)
  3. {
  4. vector<int> code;
  5. int parent;
  6. parent = huffmantree[i].parent; //先获得父节点的下标找到父节点
  7. while(parent != -1) //parent == -1 时,表明已经到了根节点了
  8. {
  9. if(i == huffmantree[parent].lchild)
  10. code.insert(code.begin(), 0);
  11. else
  12. code.insert(code.begin(), 1);
  13. i = parent; //把父节点换成当前的子节点
  14. parent = huffmantree[i].parent; //沿父指针上溯
  15. }
  16. return code;
  17. }

编码函数:

  1. //编码,并把结果存在codes向量里面,并用hash表存储,便于读取
  2. int hash[256];
  3. memset(hash,-1,sizeof(hash));
  4. vector<string> codes;
  5. vector<int> code;
  6. cout<<endl<<"字符编码如下:"<<endl;
  7. for( i=0; i<HT.GetN(); i++)
  8. {
  9. char c=HT.GetData(i); cout<<c<<": ";
  10. hash[c]=i;
  11. code = HT.GetCode(i);
  12. string tmp;
  13. for(int j=0; j<code.size(); j++)
  14. tmp+=(char)(code[j]+48);
  15. codes.push_back(tmp);
  16. cout<<tmp<<endl;
  17. }

下面是生成编码字符串

  1. string res;
  2. for ( i=0;i<rst.length();i++)
  3. {
  4. char c=rst.at(i);
  5. res+=codes[hash[c]];
  6. }
  7. ofstream out;
  8. out.open("code.txt");
  9. out<<res;
  10. out.close();
  11. cout<<endl<<"编译码已经写入code.txt!"<<endl;

2.6、解码

根据01编码进行解码。

  1. template<class T>
  2. void HuffmanTree<T>::DeCode(string source)
  3. {
  4. int root = huffmantree.size()-1; //获得根节点下标
  5. int p = root; //p为当前结点的下标
  6. for(int i=0; i<source.size(); i++)
  7. {
  8. if(source.at(i) == '0')
  9. p = huffmantree[p].lchild;
  10. else
  11. p = huffmantree[p].rchild;
  12. if( (huffmantree[p].lchild == -1) && (huffmantree[p].rchild == -1) ) //如果到了叶子节点
  13. {
  14. cout<<huffmantree[p].data;
  15. p = root; //当前结点再次是根结点
  16. }
  17. }
  18. }
  1. //下面是译码
  2. cout<<endl<<"译码结果:"<<endl;
  3. HT.DeCode(res);
  4. cout<<endl;

2.7、打印

为了方便查看哈夫曼树的结构,设计了这个方法:

  1. template<class T>
  2. void HuffmanTree<T>::Print()
  3. {
  4. cout<<"编号"<<"\t"<<"符号"<<"\t"<<"频率"<<"\t"<<"父结点"<<"\t"<<"左孩子"<<"\t"<<"右孩子"<<endl;
  5. for(int i=0; i<2*n-1; i++)
  6. {
  7. cout<<i<<"\t";
  8. if (i<n)
  9. {
  10. cout<<huffmantree[i].data<<"\t";
  11. }
  12. else
  13. {
  14. cout<<"\t";
  15. }
  16. cout<<setprecision(4)<<huffmantree[i].weight<<"%"<<"\t";
  17. cout<<huffmantree[i].parent<<"\t";
  18. cout<<huffmantree[i].lchild<<"\t";
  19. cout<<huffmantree[i].rchild<<"\t";
  20. cout<<endl;
  21. }
  22. }

2.8、保存哈夫曼树

为了讲编码翻译成源码,需要有哈夫曼树的存在,本来可以用对象的序列化存为二进制文件更加安全的,这边还是用文件的写保存。

  1. template<class T>
  2. void HuffmanTree<T>::Save(char *fname)
  3. {
  4. ofstream out;
  5. out.open(fname);
  6. out<<n<<endl;
  7. for(int i=0; i<2*n-1; i++)
  8. {
  9. if (i<n)
  10. {
  11. out<<huffmantree[i].data<<"\t";
  12. }
  13. else
  14. {
  15. out<<0<<"\t";
  16. }
  17. out<<setprecision(4)<<huffmantree[i].weight<<"\t";
  18. out<<huffmantree[i].parent<<"\t";
  19. out<<huffmantree[i].lchild<<"\t";
  20. out<<huffmantree[i].rchild<<"\t";
  21. out<<endl;
  22. }
  23. out.close();
  24. cout<<endl<<"该树已经写入"<<fname<<endl;
  25. }

2.9、还原哈夫曼树

也就是按照文件读入来构造哈夫曼树,实质上是构造函数,可能对于后于需要的其他功能提供方便。

  1. template<class T>
  2. HuffmanTree<T>::HuffmanTree(char *fname)
  3. {
  4. ifstream in;
  5. in.open(fname);
  6. in>>n; //叶子节点个数赋值给n;
  7. huffmantree.resize(2*n-1); //为为分支结点预留向量空间
  8. for(int i=0; i<2*n-1; i++)
  9. {
  10. char c;
  11. in>>c;
  12. if (c!='0')
  13. {
  14. huffmantree[i].data = c;
  15. }
  16. in>>huffmantree[i].weight;
  17. in>>huffmantree[i].parent;
  18. in>>huffmantree[i].lchild;
  19. in>>huffmantree[i].rchild;
  20. }
  21. }

三、测试函数

  1. int main()
  2. {
  3. //统计字频
  4. vector<char> s; //字符集
  5. vector<double> weight;//每个字符个数
  6. vector<HuffmanNode<char> > huffnode;//存每个结点
  7. string rst;
  8. ifstream in;
  9. cout<<"字符串内容来源于data.txt!"<<endl;
  10. in.open("data.txt");
  11. string tmp;
  12. while(getline(in,tmp))
  13. {
  14. rst+=tmp;
  15. }
  16. in.close();
  17. //cout << "请输入一个字符串:";
  18. //cin >> rst;
  19. //对rst做频率统计
  20. int frequency[256];//用于记录每个ASCII出现的次数
  21. memset(frequency,0,sizeof(frequency));
  22. for (int i = 0; i < rst.length(); i++)
  23. frequency[rst[i]]++; //每出现一次,次数加1
  24. for ( i = 0; i < 255; i++)
  25. {
  26. if (frequency[i] != 0)
  27. {
  28. weight.push_back(frequency[i]*1.0/rst.length()*100); //当频率不为0的时候,压入权重向量//这个做了个规约处理
  29. s.push_back(i); //同时把字符对应的ASCII值存到S向量
  30. }
  31. }
  32. for ( i = 0; i < s.size(); i++) //把组织好的值压入向量中,供构造树。
  33. {
  34. HuffmanNode<char> tmp={ s[i], weight[i], -1, -1, -1 };
  35. huffnode.push_back(tmp);
  36. }
  37. HuffmanNode<char> *hn=new HuffmanNode<char> [s.size()];
  38. for (i=0;i<s.size();i++)
  39. {
  40. hn[i]=huffnode[i];
  41. }
  42. //建树
  43. vector< HuffmanNode<char> > leafs(hn,hn+s.size());
  44. HuffmanTree<char> HT(leafs);
  45. cout<<"此哈夫曼树存储结构如下:"<<endl;
  46. HT.Print();
  47. //编码,并把结果存在codes向量里面,并用hash表存储,便于读取
  48. int hash[256]; //存的是字符对应在codes的位置。
  49. memset(hash,-1,sizeof(hash));
  50. vector<string> codes;//存的是字符对应的码值
  51. vector<int> code;
  52. cout<<endl<<"字符编码如下:"<<endl;
  53. for( i=0; i<HT.GetN(); i++)
  54. {
  55. char c=HT.GetData(i); cout<<c<<": ";
  56. hash[c]=i;
  57. code = HT.GetCode(i);
  58. string tmp;
  59. for(int j=0; j<code.size(); j++)
  60. tmp+=(char)(code[j]+48);
  61. codes.push_back(tmp);
  62. cout<<tmp<<endl;
  63. }
  64. //下面是生成编码字符串
  65. string res;
  66. for ( i=0;i<rst.length();i++)
  67. {
  68. char c=rst.at(i);
  69. res+=codes[hash[c]];
  70. }
  71. ofstream out;
  72. out.open("code.txt");
  73. out<<res;
  74. out.close();
  75. cout<<endl<<"编译码已经写入code.txt!"<<endl;
  76. //下面是译码
  77. cout<<endl<<"译码结果:"<<endl;
  78. HT.DeCode(res);
  79. cout<<endl;
  80. delete hn;
  81. return 0;
  82. }

测试一:提供源码文件,统计频率,输出编码,并译码。
此处输入图片的描述

  1. HuffmanTree<char> ht("MyTree.txt");
  2. ht.Print();
  3. cout<<endl;
  4. string code;
  5. ifstream in;
  6. in.open("code.txt");
  7. string tmp;
  8. while(getline(in,tmp))
  9. {
  10. code+=tmp;
  11. }
  12. in.close();
  13. ht.DeCode(code);
  14. cout<<endl;

测试二:提供哈夫曼树,提供编码,译码(有误?)
此处输入图片的描述
这边空格被误识别,原因未知。

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注