数据结构 第14讲 神秘电报密码——哈夫曼编码

本文来源于本人著作《趣学算法》。

看过谍战电影《风声》的观众都会对影片中神奇的消息传递惊叹不已!吴志国大队长在受了残忍的“针刑”之后躺在手术台上唱空城计,变了音调,把消息传给了护士,顾晓梦在衣服上缝补了长短不一的针脚……那么,片中无处不在的摩尔斯码到底是什么?它又有着怎样的神秘力量呢?

摩尔斯电码(Morse code)由点dot(. )、划dash(-)两种符号组成。它的基本原理是:把英文字母表中的字母、标点符号和空格按照出现的频率排序,然后用点和划的组合来代表这些字母、标点符号和空格,使频率最高的符号具有最短的点划组合。

图2-30 神秘电报密码

2.6.1 问题分析

我们先看一个生活中的例子:

有一群退休的老教授聚会,其中一个老教授带着刚会说话的漂亮小孙女,于是大家逗她:“你能猜猜我们多大了吗?猜对了有糖吃哦!”小女孩就开始猜:“你是1岁了吗?”,老教授摇摇头。“你是两岁了吗?”,老教授仍然摇摇头。“那一定是3岁了!”……大家哈哈大笑。或许我们都感觉到了小女孩的天真可爱,然而生活中的确有很多类似这样的判断。

曾经有这样一个C++设计题目:将一个班级的成绩从百分制转为等级制。一同学设计的程序为:

if(score <60) cout << "不及格"<<endl;
else if (score <70) cout << "及格"<<endl;
    else if (score <80) cout << "中等"<<endl;
       else if (score <90) cout << "良好"<<endl;
          else cout << "优秀"<<endl;

在上面程序中,如果分数小于60,我们做1次判定即可;如果分数为60~70,需要判定2次;如果分数为70~80,需要判定3次;如果分数为80~90,需要判定4次;如果分数为90~100,需要判定5次。

这段程序貌似是没有任何问题,但是我们却犯了从1岁开始判断一个老教授年龄的错误,因为我们的考试成绩往往是呈正态分布的,如图2-31所示。

图2-31 运行结果

也就是说,大多数(70%)人的成绩要判断3次或3次以上才能成功,假设班级人数为100人,则判定次数为:

100×10%×1+100×20%×2+100×40%×3+100×20%×4+100×10%×5=300(次)

如果我们改写程序为:

if(score <80) 
   if (score <70) 
       if (score <60) cout << "不及格"<<endl;
       else cout << "及格"<<endl;
   else cout << "中等"<<endl;
else if (score <90) cout << "良好"<<endl;
   else cout << "优秀"<<endl;

则判定次数为:

100×10%×3+100×20%×3+100×40%×2+100×20%×2+100×10%×2=230(次)

为什么会有这样大的差别呢?我们来看两种判断方式的树形图,如图2-32所示。

图2-32 两种判断方式的树形图

从图2-32中我们可以看到,当频率高的分数越靠近树根(先判断)时,我们只用1次猜中的可能性越大。

再看五笔字型的编码方式:

我们在学习五笔时,需要背一级简码。所谓一级简码,就是指25个汉字,对应着25个按键,打1个字母键再加1个空格键就可打出来相应的字。为什么要这样设置呢?因为根据文字统计,这25个汉字是使用频率最高的。

五笔字根之一级简码:

G 一 F 地 D 在  S 要  A 工

H 上 J 是 K 中  L 国  M 同

T 和 R 的 E 有  W 人 Q 我

Y 主 U 产 I 不  O 为  P 这

N 民 B 了 V 发 C 以  X 经

通常的编码方法有固定长度编码和不等长度编码两种。这是一个设计最优编码方案的问题,目的是使总码长度最短。这个问题利用字符的使用频率来编码,是不等长编码方法,使得经常使用的字符编码较短,不常使用的字符编码较长。如果采用等长的编码方案,假设所有字符的编码都等长,则表示n个不同的字符需logn要位。例如,3个不同的字符a、b、c,至少需要2位二进制数表示,a为00,b为01,c为10。如果每个字符的使用频率相等,固定长度编码是空间效率最高的方法。

不等长编码方法需要解决两个关键问题:

(1)编码尽可能短

我们可以让使用频率高的字符编码较短,使用频率低的编码较长,这种方法可以提高压缩率,节省空间,也能提高运算和通信速度。即频率越高,编码越短

(2)不能有二义性

例如,ABCD四个字符如果编码如下。

A:0。B:1。C:01。D:10。

那么现在有一列数0110,该怎样翻译呢?是翻译为ABBA,ABD,CBA,还是CD?那么如何消除二义性呢?解决的办法是:任何一个字符的编码不能是另一个字符编码的前缀,即前缀码特性

1952年,数学家D.A.Huffman提出了根据字符在文件中出现的频率,用0、1的数字串表示各字符的最佳编码方式,称为哈夫曼(Huffman)编码。哈夫曼编码很好地解决了上述两个关键问题,被广泛应用于数据压缩,尤其是远距离通信和大容量数据存储方面,常用的JPEG图片就是采用哈夫曼编码压缩的。

2.6.2 算法设计

哈夫曼编码的基本思想是以字符的使用频率作为权构建一棵哈夫曼树,然后利用哈夫曼树对字符进行编码。构造一棵哈夫曼树,是将所要编码的字符作为叶子结点,该字符在文件中的使用频率作为叶子结点的权值,以自底向上的方式,通过n−1次的“合并”运算后构造出的一棵树,核心思想是权值越大的叶子离根越近。

哈夫曼算法采取的贪心策略是每次从树的集合中取出没有双亲且权值最小的两棵树作为左右子树,构造一棵新树,新树根节点的权值为其左右孩子结点权值之和,将新树插入到树的集合中,求解步骤如下。

(1)确定合适的数据结构。编写程序前需要考虑的情况有:

  • 哈夫曼树中没有度为1的结点,则一棵有n个叶子结点的哈夫曼树共有2n−1个结点(n−1次的“合并”,每次产生一个新结点),
  • 构成哈夫曼树后,为求编码,需从叶子结点出发走一条从叶子到根的路径。
  • 译码需要从根出发走一条从根到叶子的路径,那么我们需要知道每个结点的权值、双亲、左孩子、右孩子和结点的信息。

(2)初始化。构造n棵结点为n个字符的单结点树集合T={t1t2t3,…,tn},每棵树只有一个带权的根结点,权值为该字符的使用频率。

(3)如果T中只剩下一棵树,则哈夫曼树构造成功,跳到步骤(6)。否则,从集合T中取出没有双亲且权值最小的两棵树titj,将它们合并成一棵新树zk,新树的左孩子为ti,右孩子为tjzk的权值为titj的权值之和。

(4)从集合T中删去titj,加入zk

(5)重复以上(3)~(4)步。

(6)约定左分支上的编码为“0”,右分支上的编码为“1”。从叶子结点到根结点逆向求出每个字符的哈夫曼编码,从根结点到叶子结点路径上的字符组成的字符串为该叶子结点的哈夫曼编码。算法结束。

2.6.3 完美图解

假设我们现在有一些字符和它们的使用频率(见表2-13),如何得到它们的哈夫曼编码呢?

表2-13 字符频率

字符 a b c d e f
频率 0.05 0.32 0.18 0.07 0.25 0.13

我们可以把每一个字符作为叶子,它们对应的频率作为其权值,为了比较大小方便,可以对其同时扩大100倍,得到a~f分别对应5、32、18、7、25、13。

(1)初始化。构造n棵结点为n个字符的单结点树集合T={a,b,c,d,e,f},如图2-33所示。

图2-33 叶子结点

(2)从集合T中取出没有双亲的且权值最小的两棵树a和d,将它们合并成一棵新树t1,新树的左孩子为a,右孩子为d,新树的权值为a和d的权值之和为12。新树的树根t1加入集合T,a和d从集合T中删除,如图2-34所示。

图2-34 构建新树

(3)从集合T中取出没有双亲的且权值最小的两棵树t1和f,将它们合并成一棵新树t2,新树的左孩子为t1,右孩子为f,新树的权值为t1和f的权值之和为25。新树的树根t2加入集合T,将t1和f从集合T中删除,如图2-35所示。

图2-35 构建新树

(4)从集合T中取出没有双亲且权值最小的两棵树c和e,将它们合并成一棵新树t3,新树的左孩子为c,右孩子为e,新树的权值为c和e的权值之和为43。新树的树根t3加入集合T,将c和e从集合T中删除,如图2-36所示。

图2-36 构建新树

(5)从集合T中取出没有双亲且权值最小的两棵树t2和b,将它们合并成一棵新树t4,新树的左孩子为t2,右孩子为b,新树的权值为t2和b的权值之和为57。新树的树根t4加入集合T,将t2和b从集合T中删除,如图2-37所示。

图2-37 构建新树

(6)从集合T中取出没有双亲且权值最小的两棵树t3t4,将它们合并成一棵新树t5,新树的左孩子为t4,右孩子为t3,新树的权值为t3t4的权值之和为 100。新树的树根t5加入集合T,将t3t4从集合T中删除,如图 2-38所示。

图2-38 哈夫曼树

(7)T中只剩下一棵树,哈夫曼树构造成功。

(8)约定左分支上的编码为“0”,右分支上的编码为“1”。从叶子结点到根结点逆向求出每个字符的哈夫曼编码,从根结点到叶子结点路径上的字符组成的字符串为该叶子结点的哈夫曼编码,如图2-39所示。

图2-39 哈夫曼编码

2.6.4 伪代码详解

在构造哈夫曼树的过程中,首先给每个结点的双亲、左孩子、右孩子初始化为−1,找出所有结点中双亲为−1、权值最小的两个结点t1t2,并合并为一棵二叉树,更新信息(双亲结点的权值为t1t2权值之和,其左孩子为权值最小的结点t1,右孩子为次小的结点t2t1t2的双亲为双亲结点的编号)。重复此过程,构造一棵哈夫曼树。

(1)数据结构

每个结点的结构包括权值、双亲、左孩子、右孩子、结点字符信息这 5 个域。如图 2-40所示,定义为结构体形式,定义结点结构体HnodeType

typedef struct
{
     double weight; //权值
     int parent;  //双亲
     int lchild;  //左孩子
     int rchild;  //右孩子
     char value; //该节点表示的字符
} HNodeType;

图2-40 结点结构体

在编码结构体中,bit[]存放结点的编码,start 记录编码开始下标,逆向译码(从叶子到根,想一想为什么不从根到叶子呢?)。存储时,startn−1开始依次递减,从后向前存储;读取时,从start+1开始到n−1,从前向后输出,即为该字符的编码。如图2-41所示。

图2-41 编码数组

编码结构体HcodeType

typedef struct
{
     int bit[MAXBIT]; //存储编码的数组
     int start;       //编码开始下标
} HCodeType;          /* 编码结构体 */

(2)初始化

初始化存放哈夫曼树数组HuffNode[]中的结点(见表2-14):

for (i=0; i<2*n-1; i++){
     HuffNode[i].weight = 0;//权值
     HuffNode[i].parent =-1; //双亲
     HuffNode[i].lchild =-1; //左孩子
     HuffNode[i].rchild =-1; //右孩子
}

表2-14 哈夫曼树构建数组

输入n个叶子结点的字符及权值:

for (i=0; i<n; i++){
     cout<<"Please input value and weight of leaf node "<<i + 1<<endl;
     cin>>HuffNode[i].value>>HuffNode[i].weight;
}

(3)循环构造Huffman树

从集合T中取出双亲为−1且权值最小的两棵树titj,将它们合并成一棵新树zk,新树的左孩子为ti,右孩子为tjzk的权值为titj的权值之和。

    int i, j, x1, x2; //x1、x2为两个最小权值结点的序号。
    double m1,m2; //m1、m2为两个最小权值结点的权值。
    for (i=0; i<n-1; i++){
         m1=m2=MAXVALUE;  //初始化为最大值
         x1=x2=-1;  //初始化为-1
         //找出所有结点中权值最小、无双亲结点的两个结点
         for (j=0; j<n+i; j++){
              if (HuffNode[j].weight < m1 && HuffNode[j].parent==-1){
                   m2 = m1;
                   x2 = x1;
                   m1 = HuffNode[j].weight;
                   x1 = j;
              }
              else if (HuffNode[j].weight < m2 && HuffNode[j].parent==-1){
                   m2=HuffNode[j].weight;
                   x2=j;
              }
         }
         /* 更新新树信息 */
         HuffNode[x1].parent = n+i; //x1的父亲为新结点编号n+i
         HuffNode[x2].parent = n+i; //x2的父亲为新结点编号n+i
         HuffNode[n+i].weight =m1+m2; //新结点权值为两个最小权值之和m1+m2
         HuffNode[n+i].lchild = x1; //新结点n+i的左孩子为x1 
         HuffNode[n+i].rchild = x2; //新结点n+i的右孩子为x2
    }
}

图解:

(1)i=0时,j=0;j<6;找双亲为−1,权值最小的两个数:

x1=0   x2=3//x1、x2为两个最小权值结点的序号
m1=5  m2=7//m1、m2为两个最小权值结点的权值
HuffNode[0].parent = 6;   //x1的父亲为新结点编号n+i
HuffNode[3].parent = 6;   //x2的父亲为新结点编号n+i
HuffNode[6].weight =12;   //新结点权值为两个最小权值之和m1+m2
HuffNode[6].lchild = 0;   //新结点n+i的左孩子为x1 
HuffNode[6].rchild = 3;   //新结点n+i的右孩子为x2

数据更新后如表2-15所示。

表2-15 哈夫曼树构建数组

对应的哈夫曼树如图2-42所示。

图2-42 哈夫曼树生成过程

(2)i=1时,j=0;j<7;找双亲为−1,权值最小的两个数:

x1=6    x2=5//x1、x2为两个最小权值结点的序号
m1=12  m2=13//m1、m2为两个最小权值结点的权值
HuffNode[5].parent = 7;   //x1的父亲为新结点编号n+i
HuffNode[6].parent = 7;   //x2的父亲为新结点编号n+i
HuffNode[7].weight =25;   //新结点权值为两个最小权值之和m1+m2
HuffNode[7].lchild = 6;   //新结点n+i的左孩子为x1 
HuffNode[7].rchild = 5;   //新结点n+i的右孩子为x2

数据更新后如表2-16所示。

表2-16 哈夫曼树构建数组

对应的哈夫曼树如图2-43所示。

图2-43 哈夫曼树生成过程

(3)i=2时,j=0;j<8;找双亲为−1,权值最小的两个数:

x1=2    x2=4//x1、x2为两个最小权值结点的序号
m1=18  m2=25//m1、m2为两个最小权值结点的权值
HuffNode[2].parent = 8;   //x1的父亲为新结点编号n+i
HuffNode[4].parent = 8;   //x2的父亲为新结点编号n+i
HuffNode[8].weight =43;   //新结点权值为两个最小权值之和m1+m2
HuffNode[8].lchild = 2;   //新结点n+i的左孩子为x1 
HuffNode[8].rchild = 4;   //新结点n+i的右孩子为x2

数据更新后如表2-17所示。

表2-17 哈夫曼树构建数组

对应的哈夫曼树如图2-44所示。

图2-44 哈夫曼树生成过程

(4)i=3时,j=0;j<9;找双亲为−1,权值最小的两个数:

x1=7    x2=1//x1、x2为两个最小权值结点的序号
m1=25  m2=32//m1、m2为两个最小权值结点的权值
HuffNode[7].parent = 9;    //x1的父亲为新结点编号n+i
HuffNode[1].parent = 9;     //x2的父亲为新结点编号n+i
HuffNode[8].weight =57;     //新结点权值为两个最小权值之和m1+m2
HuffNode[8].lchild = 7;     //新结点n+i的左孩子为x1 
HuffNode[8].rchild = 1;     //新结点n+i的右孩子为x2

数据更新后如表2-18所示。

表2-18 哈夫曼树构建数组

对应的哈夫曼树如图2-45所示。

图2-45 哈夫曼树生成过程

(5)i=4时,j=0;j<10;找双亲为−1,权值最小的两个数:

x1=8    x2=9//x1、x2为两个最小权值结点的序号
m1=43  m2=57//m1、m2为两个最小权值结点的权值
HuffNode[8].parent = 10;  //x1的父亲为生成的新结点编号n+i
HuffNode[9].parent =10;   //x2的父亲为生成的新结点编号n+i
HuffNode[10].weight =100;  //新结点权值为两个最小权值之和m1+ m2
HuffNode[10].lchild = 8; //新结点编号n+i的左孩子为x1 
HuffNode[10].rchild = 9; //新结点编号n+i的右孩子为x2

数据更新后如表2-19所示。

表2-19 哈夫曼树构建数组

对应的哈夫曼树如图2-46所示。

图2-46 哈夫曼树生成过程

(6)输出哈夫曼编码

    void HuffmanCode(HCodeType HuffCode[MAXLEAF],  int n)
   {
    HCodeType cd;       /* 定义一个临时变量来存放求解编码时的信息 */
    int i,j,c,p;
    for(i = 0;i < n; i++){
          cd.start = n-1;
          c = i;  //i为叶子结点编号
          p = HuffNode[c].parent;
          while(p != -1){
              if(HuffNode[p].lchild == c){
                  cd.bit[cd.start] = 0;
              }
              else
                  cd.bit[cd.start] = 1;
              cd.start--;         /* start向前移动一位 */
              c = p;              /* c,p变量上移,准备下一循环 */
              p = HuffNode[c].parent;    
          }
          /* 把叶子结点的编码信息从临时编码cd中复制出来,放入编码结构体数组 */
          for (j=cd.start+1; j<n; j++)
              HuffCode[i].bit[j] = cd.bit[j];
          HuffCode[i].start = cd.start;
    }
}

图解:哈夫曼编码数组如图2-47所示。

图2-47 哈夫曼编码数组

(1)i=0时,c=0;

cd.start = n-1=5;
p = HuffNode[0].parent=6;//从哈夫曼树建成后的表HuffNode[]中读出
                         //p指向0号结点的父亲6号

构建完成的哈夫曼树数组如表2-20所示。

表2-20 哈夫曼树构建数组

如果p != −1,那么从表HuffNode[]中读出6号结点的左孩子和右孩子,判断0号结点是它的左孩子还是右孩子,如果是左孩子编码为0;如果是右孩子编码为1。

从表2-20可以看出:

HuffNode[6].lchild=0;//0号结点是其父亲6号的左孩子
cd.bit[5] = 0;//编码为0
cd.start--=4; /* start向前移动一位*/

哈夫曼编码树如图2-48所示,哈夫曼编码数组如图2-49所示。

图2-48 哈夫曼编码树

图2-49 哈夫曼编码数组

c = p=6;                      /* c、p变量上移,准备下一循环 */
p = HuffNode[6].parent=7;

cp变量上移后如图2-50所示。

p != -1;
HuffNode[7].lchild=6;//6号结点是其父亲7号的左孩子
cd.bit[4] = 0;//编码为0
cd.start--=3;        /* start向前移动一位*/
c = p=7;             /* c、p变量上移,准备下一循环 */
p = HuffNode[7].parent=9;

图2-50 哈夫曼编码树

哈夫曼编码树如图2-51所示,哈夫曼编码数组如图2-52所示。

图2-51 哈夫曼编码树

图2-52 哈夫曼编码数组

p != -1;
HuffNode[9].lchild=7;//7号结点是其父亲9号的左孩子
cd.bit[3] = 0;//编码为0
cd.start--=2;        /* start向前移动一位*/
c = p=9;             /* c、p变量上移,准备下一循环 */
p = HuffNode[9].parent=10;

哈夫曼编码树如图2-53所示,哈夫曼编码数组如图2-54所示。

p != -1;
HuffNode[10].lchild!=9;//9号结点不是其父亲10号的左孩子
cd.bit[2] = 1;//编码为1
cd.start--=1;        /* start向前移动一位*/
c = p=10;            /* c、p变量上移,准备下一循环 */
p = HuffNode[10].parent=-1;

图2-53 哈夫曼编码树

图2-54 哈夫曼编码数组

哈夫曼编码树如图2-55所示,哈夫曼编码数组如图2-56所示。

p = -1;该叶子结点编码结束。
/* 把叶子结点的编码信息从临时编码cd中复制出来,放入编码结构体数组 */
   for (j=cd.start+1; j<n; j++)
        HuffCode[i].bit[j] = cd.bit[j];
   HuffCode[i].start = cd.start;

图2-55 哈夫曼编码树

图2-56 哈夫曼编码数组

HuffCode[]数组如图2-57所示。

图2-57 哈夫曼编码HuffCode[]数组

注意:图中的箭头不表示指针。

2.6.5 实战演练

//program 2-6
#include
#include
#include
#include
using namespace std;
#define MAXBIT    100
#define MAXVALUE  10000
#define MAXLEAF   30
#define MAXNODE   MAXLEAF*2 -1
typedef struct
{
    double weight;
    int parent;
    int lchild;
    int rchild;
    char value;
} HNodeType;        /* 结点结构体 */
typedef struct
{
    int bit[MAXBIT];
    int start;
} HCodeType;        /* 编码结构体 */
HNodeType HuffNode[MAXNODE]; /* 定义一个结点结构体数组 */
HCodeType HuffCode[MAXLEAF];/* 定义一个编码结构体数组*/
/* 构造哈夫曼树 */
void HuffmanTree (HNodeType HuffNode[MAXNODE],  int n)
{
    /* i、j: 循环变量,m1、m2:构造哈夫曼树不同过程中两个最小权值结点的权值,
       x1、x2:构造哈夫曼树不同过程中两个最小权值结点在数组中的序号。
    */
    int i, j, x1, x2;
    double m1,m2;
    /* 初始化存放哈夫曼树数组 HuffNode[] 中的结点 */
    for (i=0; i<2*n-1; i++)
    {
         HuffNode[i].weight = 0;//权值
         HuffNode[i].parent =-1;
         HuffNode[i].lchild =-1;
         HuffNode[i].rchild =-1;
    }
    /* 输入 n 个叶子结点的权值 */
    for (i=0; i<n; i++)
    {
         cout<<"Please input value and weight of leaf node "<<i + 1<<endl;
         cin>>HuffNode[i].value>>HuffNode[i].weight;
    }
    /* 构造 Huffman 树 */
    for (i=0; i<n-1; i++)
    {//执行n-1次合并
         m1=m2=MAXVALUE;
         /* m1、m2中存放两个无父结点且结点权值最小的两个结点 */
         x1=x2=-1;
         /* 找出所有结点中权值最小、无父结点的两个结点,并合并之为一棵二叉树 */
         for (j=0; j<n+i; j++)
         {
              if (HuffNode[j].weight < m1 && HuffNode[j].parent==-1)
              {
                  m2 = m1;
                  x2 = x1;
                  m1 = HuffNode[j].weight;
                  x1 = j;
              }
              else if (HuffNode[j].weight < m2 && HuffNode[j].parent==-1)
              {
                  m2=HuffNode[j].weight;
                  x2=j;
              }
         }
         /* 设置找到的两个子结点 x1、x2 的父结点信息 */
         HuffNode[x1].parent  = n+i;
         HuffNode[x2].parent  = n+i;
         HuffNode[n+i].weight = m1+m2;
         HuffNode[n+i].lchild = x1;
         HuffNode[n+i].rchild = x2;
         cout<<"x1.weight and x2.weight in round "<<i+1<<"\t"<<HuffNode[x1]. weight<<"\t"<<HuffNode[x2].weight<<endl; /* 用于测试 */
    }
}
/* 哈夫曼树编码 */
void HuffmanCode(HCodeType HuffCode[MAXLEAF],  int n)
{
    HCodeType cd;       /* 定义一个临时变量来存放求解编码时的信息 */
    int i,j,c,p;
    for(i = 0;i < n; i++)
    {
        cd.start = n-1;
        c = i;
        p = HuffNode[c].parent;
        while(p != -1)
        {
            if(HuffNode[p].lchild == c)
                cd.bit[cd.start] = 0;
            else
                cd.bit[cd.start] = 1;
            cd.start--;        /* 求编码的低一位 */
            c = p;
            p = HuffNode[c].parent;    /* 设置下一循环条件 */
        }
        /* 把叶子结点的编码信息从临时编码cd中复制出来,放入编码结构体数组 */
        for (j=cd.start+1; j<n; j++)
           HuffCode[i].bit[j] = cd.bit[j];
        HuffCode[i].start = cd.start;
    }
}
int main()
{
    int i,j,n;
    cout<<"Please input n:"<<endl;
    cin>>n;
    HuffmanTree (HuffNode, n);  /* 构造哈夫曼树 */
    HuffmanCode(HuffCode, n);  /* 哈夫曼树编码 */
    /* 输出已保存好的所有存在编码的哈夫曼编码 */
    for(i = 0;i < n;i++)
    {
        cout<<HuffNode[i].value<<": Huffman code is: ";
        for(j=HuffCode[i].start+1; j < n; j++)
            cout<<HuffCode[i].bit[j];
        cout<<endl;
    }
    return 0;
}

算法实现和测试

(1)运行环境

Code::Blocks

(2)输入

Please input n
6
Please input value and weight of leaf node 1
a 0.05
Please input value and weight of leaf node 2
b 0.32
Please input value and weight of leaf node 3
c 0.18
Please input value and weight of leaf node 4
d 0.07
Please input value and weight of leaf node 5
e 0.25
Please input value and weight of leaf node 6
f 0.13

(3)输出

x1.weight and x2.weight in round 1      0.05    0.07
x1.weight and x2.weight in round 2      0.12    0.13
x1.weight and x2.weight in round 3      0.18    0.25
x1.weight and x2.weight in round 4      0.25    0.32
x1.weight and x2.weight in round 5      0.43    0.57
a: Huffman code is: 1000
b: Huffman code is: 11
c: Huffman code is: 00
d: Huffman code is: 1001
e: Huffman code is: 01
f: Huffman code is: 101

2.6.6 算法解析及优化拓展

1.算法复杂度分析

(1)时间复杂度:由程序可以看出,在函数HuffmanTree()中,if (HuffNode[j].weighti与j组成双层循环:

i=0时,该语句执行n次;

i=1时,该语句执行n+1次;

i=2时,该语句执行n+2次;

……

i=n−2时,该语句执行n+n−2次;

则基本语句共执行n+(n+1)+(n+2)+…+(n+(n−2))=(n−1)*(3n−2)/2次(等差数列);在函数HuffmanCode()中,编码和输出编码时间复杂度都接近n2;则该算法时间复杂度为O(n2)。

(2)空间复杂度:所需存储空间为结点结构体数组与编码结构体数组,哈夫曼树数组 HuffNode[]中的结点为n个,每个结点包含bit[MAXBIT]和start两个域,则该算法空间复杂度为On* MAXBIT)。

2.算法优化拓展

该算法可以从两个方面优化:

(1)函数HuffmanTree()中找两个权值最小结点时使用优先队列,时间复杂度为logn,执行n−1次,总时间复杂度为On logn)。

(2)函数HuffmanCode()中,哈夫曼编码数组HuffNode[]中可以定义一个动态分配空间的线性表来存储编码,每个线性表的长度为实际的编码长度,这样可以大大节省空间。

你可能感兴趣的:(趣学数据结构,趣学算法)