第五章 树和二叉树(5.7)

5.7哈夫曼树及其应用

5.7.1哈夫曼树的基本概念

哈夫曼(Huffman)树又称最优树,是一类带权路径长度最短的树,在实际中有广泛的用途。哈夫曼树的定义,涉及路径、路径长度、权等概念,下面先给出这些概念的定义,然后再介绍哈夫曼树。

路径:从一个结点到另一个结点之间的分支序列

​ 路径长度:从一个结点到另一个结点所经过的分支数目

​ 结点的权:根据应用的需要可以给树的结点赋权值

​ 带权路径长度:从根到该结点的路径长度与该结点权的乘积

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

​ 哈夫曼树:由n个带权叶子结点构成的所有二叉树中带权路径长度最短的二叉树

img ![](F:\极客班\数据结构-极客班\第五章 树和二叉树\image\20180412213549715.png)

5.7.2哈夫曼树的构造算法

1.哈夫曼树的构造过程

1)初始化:根据给定的n个权值 ,构造n棵只有一个根结点的二叉树, n个权值分别是这些二叉树根结点的权;

​ 2)找最小树:在F中选取两棵根结点树值最小的树作为左、右子树,构造一颗新的二叉树,置新二叉树根的权值为左、右子树根结点权值之和;

​ 3)删除与加入:从F中删除这两颗树,并将新树加入F;

​ 4)判断:重复2)和3),直到F中只含一颗树为止,此时得到的这颗二叉树就是哈夫曼树。

2.哈夫曼算法的实现

哈夫曼树是一种二叉树,当然 可以采用前面介绍过的通用存储方法,而由千哈夫曼树中没有度为 1 的结点,则一棵有 n 个叶子结点的哈夫曼树共有 2n-I 个结点,可以存储在一个大小为 2n-I的一维数组中。树中每个结点还要包含其双亲信息和孩子结点的信息,由此,每个结点的存储结构设计如图5.27所示。

image-20220128182300563

//- - - - -哈夫曼树的存储表示 --- - - .-
typedef struct{ 
int weight; //结点的权值
int parent,lchild,rchild; //结点的双亲、左孩子、右孩子的下标
} HTNode,*HuffmanTree; //动态分配数组存储哈夫曼树

哈夫曼树的各结点存储在由 HuffmanTree 定义的动态分配的数组中,为了实现方便,数组的0号单元不使用,从1号单元开始使用,所以数组的大小为 2n。将叶子结点集中存储在前面部分l~n 个位置,而后面的 n-1 个位置存储其余非叶子结点。

3.算法5.10 构造哈夫曼树

3.1算法步骤

1.初始化:首先动态申请 2n 个单元;然后循环 2n-l 次,从 1 号单元开始,依次将 12n-l所有单元中的双亲、左孩子、右孩子的下标都初始化为O; 最后再循环n次,输入前n个单元中叶子结点的权值。

2.创建树:循环 n-1 次,通过 n1 次的选择、删除与合并来创建哈夫曼树。选择是从当前森林中选择双亲为0且权值最小的两个树根结点 sls2; 删除是指将结点 sls2 的双亲改为非O; 合并就是将 sls2 的权值和作为一个新结点的权值依次存入到数组的第 n+l 之后的单元中,同时记录这个新结点左孩子的下标为 sl, 右孩子的下标为 s2

3.2算法描述

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

5.7.3哈夫曼编码

1.哈夫曼编码的主要思想

1.1概念

(l)前缀编码:如果在一个编码方案中,任一个编码都不是其他任何编码的前缀(最左子串),则称编码是前缀编码。 例 如, 案例5.1中的第2种编码方案(见表5.1 (b))的编码 0, 10, 110, 111是前缀编码, 而 第3种编码方案(见表5.1 (c)) 的编码 0, 01, 010, 111就不是前缀编码。前缀编码可以保证对压缩文件进行解码时不产生二义性, 确保正确解码。

(2) 哈夫曼编码:对一棵具有n个叶子的哈夫曼树,若对树中的每个左分支 赋予0, 右分支赋予1'则从根到每个叶子的路径上,各分支 的赋值分别构成一个二进制串, 该二进制串就称为哈夫曼编码。

1.2性质

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

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

2.哈夫曼编码算法实现

2.1算法步骤

1.分配存储n个字符编码的编码表空间HC, 长度为n+l; 分配临时存储每个字符编码的动

态数组空间cd, cd[n-1)置为'\O'。

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

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

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

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

► 回溯一次start向前指一个位置,即- -start;

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

► 继续向上回溯,改变c和f的值。-·

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

3.释放临时空间cd。

2.2算法描述

void CreatHuffmanCode(HuffmanTree HT,HuffmanCode &HC,int n) 
{//从叶子到根逆向求每个字符的哈夫曼编码, 存储在编码表HC中
HC=new char* [n+l]; //分配存储n个字符编码的编码表空间
cd=new char [n]; //分配临时存放每个字符编码的动态数组空间
cd[n-1]='\0'; //编码结束符
for(i=l;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]='l'; //结点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; //释放临时空间
}

3.文件的编码和译码

1.编码

有了字符集的哈夫曼编码表之后,对数据文件的编码过程是:依次读入文件中的字符c, 在哈夫曼编码表HC中找到此字符,将字符c转换为编码表中存放的编码串

2.译码

对编码后的文件进行译码的过程必须借助千哈夫曼树。具体过程是:依次读入文件的二进制码,从哈夫曼树的根结点(即HT[m])出发,若当前读入 0, 则走向左孩子,否则走向右孩子。一旦到达某一叶子HT[i]时便译出相应的字符编码HC[i]。然后重新从根出发继续译码,直至文件结束。

posted @     阅读(75)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 全程使用 AI 从 0 到 1 写了个小工具
· 快收藏!一个技巧从此不再搞混缓存穿透和缓存击穿
· AI 插件第二弹,更强更好用
· Blazor Hybrid适配到HarmonyOS系统
· 支付宝 IoT 设备入门宝典(下)设备经营篇
点击右上角即可分享
微信分享提示