0
点赞
收藏
分享

微信扫一扫

数据结构-哈夫曼树详解(类C语言版)



文章目录

  • ​​引入哈夫曼树​​
  • ​​例​​
  • ​​引出问题​​
  • ​​哈夫曼树的基本概念​​
  • ​​路径​​
  • ​​结点的路径长度​​
  • ​​树的路径长度​​
  • ​​权(weight)​​
  • ​​结点的带权路径长度​​
  • ​​树的带权路径长度​​
  • ​​例​​
  • ​​什么是哈夫曼树​​
  • ​​注意​​
  • ​​哈夫曼树的构造算法​​
  • ​​哈夫曼算法(构造哈夫曼树的方法)​​
  • ​​例1​​
  • ​​例2​​
  • ​​总结​​
  • ​​哈夫曼构造算法的实现​​
  • ​​结点类型定义​​
  • ​​例​​
  • ​​哈夫曼树构造算法的实现​​
  • ​​例​​
  • ​​哈夫曼编码​​
  • ​​前缀编码​​
  • ​​引出哈夫曼编码​​
  • ​​例题​​
  • ​​两个问题​​
  • ​​哈夫曼编码的存储表示​​
  • ​​根据哈夫曼树求哈夫曼编码​​
  • ​​哈夫曼编码应用举例​​

引入哈夫曼树

编程:将学生的百分制成绩转换为五分制成绩。

<60:E 60-69:D 70-79:C 80-89:B 90-100:A

if(score<60)
grade == 'E';
else if(score<70)
grade == 'D';
else if(score<80)
grade == 'C';
else if(score<90)
grade == 'B';
else
grade == 'A';

数据结构-哈夫曼树详解(类C语言版)_结点

当树上带权值时:

5%的数据需1次比较,15%的数据需2次比较,40%的数据需3次比较,40%的数据需4次比较,因此10000个数据比较的次数为:

10000(1 * 5% + 2 * 15% + 3 * 40% + 4 * 10%) = 31500次

10000(3 * 20% + 2 * 80%) = 22000次

数据结构-哈夫曼树详解(类C语言版)_数据结构_02

引出问题

能不能找到一种效率最高的判别树呢?

答:哈夫曼树(最优二叉树)

(判断树:用于描述分类过程的二叉树。)

哈夫曼树的基本概念

路径

从树中一个结点到另一个结点之间的分支构成这两个结点间的路径。

结点的路径长度

两结点间路径上的分支数。

数据结构-哈夫曼树详解(类C语言版)_c语言_03

(a)从A到B, C, D, E, F, G, H, I的路径长度分别为1, 1, 2, 2, 3, 3, 4, 4。

数据结构-哈夫曼树详解(类C语言版)_结点_04

(b)从A到B, C, D, E, F, G, H, I的路径长度分别为1, 1, 2, 2, 2, 2, 3, 3。

树的路径长度

从树根到每一个结点的路径长度之和。记作TL。

TL(a) = 0+1+1+2+2+3+3+4+4=20

TL(b) = 0+1+1+2+2+2+2+3+3=16

结点数目相同的二叉树中,完全二叉树是路径长度最短的二叉树。

权(weight)

将树中结点赋给一个有着某种含义的数值,则这个数值称为该结点的权。

结点的带权路径长度

从根结点到该节点之间的路径长度与该结点的权的乘积。

树的带权路径长度

树中所有叶子结点的带权路径长度之和。

记作:数据结构-哈夫曼树详解(类C语言版)_哈夫曼编码_05

其中,wk为权值,lk为结点到根的路径长度。

有4个结点a, b, c, d,权值分别为7, 5, 2, 4,构造以此4个结点为叶子结点的二叉树:

数据结构-哈夫曼树详解(类C语言版)_数据结构_06

带权路径长度是:

(a)WPL=7x2 + 5x2 + 2x2 + 4x2 = 36

(b)WPL=7x3 + 5x3 + 2x1 + 4x2 = 46

什么是哈夫曼树

哈夫曼树:最优树,就是带权路径长度(WPL)最短的树。

注意:“带权路径长度最短”是在“度相同”的树中比较而得的结果,因此有最优二叉树、最优三叉树之称等等。

哈夫曼树:最优二叉树,就是带权路径长度(WPL)最短的二叉树。

因为构造这种树的算法是由哈夫曼教授于1952年提出的,所以被称为哈夫曼树,相应的算法称为哈夫曼算法。

数据结构-哈夫曼树详解(类C语言版)_结点_07

注意

满二叉树不一定是哈夫曼树。

哈夫曼树中权越大的叶子离根越近。

具有相同带权结点的哈夫曼树不唯一。

哈夫曼树的构造算法

数据结构-哈夫曼树详解(类C语言版)_c语言_08

哈夫曼树中权越大的叶子离根越近。

贪心算法:构造哈夫曼树时首先选择权值小的叶子结点。

哈夫曼算法(构造哈夫曼树的方法)

(1)根据n个给定的权值(w1, w2, …, wn)构成n棵二叉树的森林F = {T1, T2, …, Tn},其中,Ti只有一个带权为wi的根节点。

构造森林全是根

(2)在F中选取两棵根结点的权值最小的树作为左右子树,构造一棵新的二叉树,且设置新的二叉树的根结点的权值为其左右子树上根结点的权值之和。

选用两小造新树

(3)在F中删除这两棵树,同时将新得到的二叉树加入森林中。

删除两小添新人

(4)重复(2)和(3),直到森林中只有一棵树位置,这棵树即为哈夫曼树。

重复2、3剩单根

例1

有4个结点a, b, c, d,权值分别为7, 5, 2, 4,构造哈夫曼树。

数据结构-哈夫曼树详解(类C语言版)_霍夫曼树_09

例2

有5个结点a, b, c, d, e,权值分别为7, 5, 5, 2, 4,构造哈夫曼树。

数据结构-哈夫曼树详解(类C语言版)_c语言_10

总结

哈夫曼树的结点度为0或2,没有度为1的结点。

包含n棵树的森林要经过n-1次合并才能形成哈夫曼树,共产生n-1个新结点。

包含n个叶子结点的哈夫曼树中共有2n-1个结点。

在哈夫曼算法中,初始有n棵二叉树,要经过n-1次合并最终形成哈夫曼树。

经过n-1次合并产生n-1个新结点,且这n-1个新结点都是具有两个孩子的分支结点。

可见:哈夫曼树中共有n+n-1 = 2n-1个结点,且其所有的分支结点的度均不为1。

哈夫曼构造算法的实现

采用顺序存储结构——一维结构数组(HuffmanTree H;)

结点类型定义

typedef struct {
int weight;
int parent, lch, rch;
} HTNode, *HuffmanTree;

数据结构-哈夫曼树详解(类C语言版)_哈夫曼编码_11

哈夫曼树中共有2n-1个结点,不适用0下标,数组大小为2n。

例如,第1个结点权值为5,即可表示为H[i].weight=5。

有n=8,权值为W={7, 19, 2, 6, 32, 3, 21, 10},构造哈夫曼树。

数据结构-哈夫曼树详解(类C语言版)_数据结构_12

数据结构-哈夫曼树详解(类C语言版)_数据结构_13

哈夫曼树构造算法的实现

1.初始化HT[1…2n-1]:lch=rch=parent=0;

2.输入初始n个叶子结点:置HT[1…n]的weight值;

3.进行以下n-1次合并,依次产生n-1个结点HT[i],i=n+1…2n-1:

(1)在HT[1…i-1]中选两个未被选过(从parent == 0的结点中选)的weight最小的两个结点HT[s1]和HT[s2], s1、s2为两个最小结点下标;

(2)修改HT[s1]和HT[s2]的parent值:HT[s1].parent=i;HT[s2].parent=i;

(3)修改新产生的HT[i]:

① HT[i].weight = HT[s1].weight + HT[s2].weight;

② HT[i].lch = s1;HT[i].rch = s2;

void CreateHuffmanTree(HuffmanTree &HT, int n) 
{ //构造哈夫曼树 HT
if(n<=1) return;
m=2*n-l;
HT=new HTNode[m+l]; //0 号单元未用,所以需要动态分配 m+l 个单元, HT[m]表示根结点
for(i=1;i<=m;++i) //将1~m号单元中的双亲、左孩子,右孩子的下标都初始化为0
{HT[i].parent=O; HT[i].lchild=O; HT[i].rchild=O;}
for(i=1;i<=n;++i} //输人前 n 个单元中叶子结点的权值
cin>>HT[i].weight;
* ---- ----- -初始化工作结束, 下面开始创建哈夫曼树- ------ */
for (i=n+1; i<=m; ++i}
{//通过 n-1 次的选择、删除 、 合并来创建哈夫曼树
Select (HT, i-1, s1, s2};
//在 HT[k] (1≤k≤i-1) 中选择两个其双亲域为0 且权值最小的结点,并返回它们在 HT 中的序号 s1和 s2
HT[s1].parent=i;HT[s2].parent=i;
//得到新结点 i, 从森林中删除s1, s2, 将s1和s2 的双亲域由 0改为l.
HT[i].lchild=s1;HT[i].rchild=s2; //s1, s2分别作为i的左右孩子
HT[i].weight=HT[s1].weight+HT[s2].weight; //i的权值为左右孩子权值之和
}//for
}

已知 w = (5,29,7,8,14,23,3,11), 利用算法试构造一棵哈夫曼树, 计算树的带权路径长度, 并给出其构造过程中存储结构HT的初始状态和终结状态。

n= 8, 则 m = 15, 按算法可构造一棵哈夫曼树, 如图所示。

数据结构-哈夫曼树详解(类C语言版)_c语言_14

树的带权路径长度计算如下:

数据结构-哈夫曼树详解(类C语言版)_霍夫曼树_15

其存储结构HT的初始状态如表 5.2 (a)所示, 其终结状态如表 5.2 (b)所示。

数据结构-哈夫曼树详解(类C语言版)_数据结构_16

哈夫曼编码

在远程通讯中,要将待传字符转换成由二进制的字符串:

若编码为:A-00; B-01; C-10; D-11

则要传送的字符为:

数据结构-哈夫曼树详解(类C语言版)_霍夫曼树_17

若将编码设计为长度不等的二进制编码,即让待传字符串中出现次数较多的字符采用尽可能短的编码,则转换的二进制字符串便可能减少。

前缀编码

数据结构-哈夫曼树详解(类C语言版)_数据结构_18

关键:要设计长度不等的编码,则必须使任一字符的编码都不是另一个字符的编码的前缀,这种编码称作前缀编码。

引出哈夫曼编码

问题:什么样的前缀码能使得电文总长最短?

——哈夫曼编码。

方法:

1.统计字符集中每个字符在电文中出现的平均概率(概率越大,要求编码越短)。

2.利用哈夫曼树的特点:权越大的叶子离根越近;将每个字符的概率值作为权值,构造哈夫曼树。则概率越大的结点,路径越短。

3.在哈夫曼树的每个分支上标上0或1:

结点的左分支标0,右分支标1。

把从根到每个叶子的路径上的标号连接起来,作为该叶子代表的字符的编码。

例题

要传输的字符集D = {C, A, S, T, ; }

字符出现频率w = {2, 4, 2, 3, 3 }

数据结构-哈夫曼树详解(类C语言版)_霍夫曼树_19

例:电文是{CAS;CAT;SAT;AT}

其编码是:11010111011101000011111000011000,反之,若编码是“1101000”,则其译文是“CAT”。

两个问题

1.为什么哈夫曼编码能够保证是前缀编码?

因为没有一片树叶是另一片树叶的祖先,所以每个叶结点的编码就不可能是其它叶结点编码的前缀。

2.为什么哈夫曼编码能够保证字符编码总长最短?

因为哈夫曼树的带权路径长度最短,故字符编码的总长最短。

· 性质1:哈夫曼编码是前缀编码。

· 性质2:哈夫曼编码是最优前缀编码。

哈夫曼编码的存储表示

// - - - - -哈夫曼编码表的存储表示-----
typedef char **HuffmanCode; // 动态分配数组存储哈夫曼编码表

根据哈夫曼树求哈夫曼编码

算法步骤:

1.分配存储n个字符编码的编码表空间HC,长度为n+1;分配临时存储每个字符编码的动态数组空间cd,cd[n-1]置为’\0’。

2.逐个求解n个字符的编码,循环n次,执行以下操作:

(1)设置变量start用于记录编码在cd中存放的位置,start初始时指向最后,即编码结束符位置n-1;

(2)设置变量c用于记录从叶子结点向上回溯至根结点所经过的结点下标,c初始时为当前待编码字符的下标i,f用于记录i的双亲结点的下标;

(3)从叶子结点向上回溯至根结点,求得字符i的编码,当f没有到达根结点时,循环执行以下操作:

① 回溯依次start向前指一个位置,即–start;

② 若结点c是f的左孩子,则生成代码0,否则生成代码1,生成的代码0或1保存在cd[start]中;

③ 继续向上回溯,改变c和f的值。

(4)根据数组cd的字符串长度为第i个字符编码分配空间HC[i],然后将数组cd中的编码复制到HC[i]中。

3.释放临时空间cd。

void CreatHuffmanCode(HuffmanTree HT,HuffmanCode &HC,int n) 
{ // 从叶子到根逆向求每个字符的哈夫曼编码, 存储在编码表HC中
HC=new char*[n+1]; // 分配存储n个字符编码的编码表空间
cd=new char[n]; // 分配临时存放每个字符编码的动态数组空间
cd[n-1]='\0'; // 编码结束符
for(i=1;i<=n;++i) // 逐个字符求哈夫曼编码
{
start=n-1; //start 开始时指向最后, 即编码结束符位置
c=i; f=HT[i].parent; //f指向结点c的双亲结点
while(f!=O) // 从叶子结点开始向上回溯, 直到根结点
{
--start; //回溯一次start向前指一个位置
if(HT[f].lchild==c) cd[start]='O'; //结点c是f的左孩子, 则生成代码0
else cd[start]='1'; //结点c是f的右孩子, 则生成代码1
c=f;f=HT[f].parent; //继续向上回溯
} //求出第l.个字符的编码
HC[i]=new char[n-start]; //为第i个字符编码分配空间
strcpy(HC[i],&cd[start]); //将求得的编码从临时空间cd复制到HC的当前行中
}
delete cd; //释放临时空间
}

哈夫曼编码应用举例

哈夫曼树─即最优二叉树,带权路径长度最小的二叉树,经常应用于数据压缩。 在计算机信息处理中,“哈夫曼编码”是一种一致性编码法(又称“熵编码法”),用于数据的无损耗压缩。这一术语是指使用一张特殊的编码表将源字符(例如某文件中的一个符号)进行编码。这张编码表的特殊之处在于,它是根据每一个源字符出现的估算概率而建立起来的(出现概率高的字符使用较短的编码,反之出现概率低的则使用较长的编码,这便使编码之后的字符串的平均期望长度降低,从而达到无损压缩数据的目的)。这种方法是由David.A.Huffman发展起来的。 例如,在英文中,e的出现概率很高,而z的出现概率则最低。当利用哈夫曼编码对一篇英文进行压缩时,e极有可能用一个位(bit)来表示,而z则可能花去25个位(不是26)。用普通的表示方法时,每个英文字母均占用一个字节(byte),即8个位。二者相比,e使用了一般编码的1/8的长度,z则使用了3倍多。若能实现对于英文中各个字母出现概率的较准确的估算,就可以大幅度提高无损压缩的比例。



举报

相关推荐

0 条评论