文章目录

  • 前言
  • 一、哈夫曼树的基本概念
    • 1. 哈夫曼树的特点
  • 二、哈夫曼树的构造算法
    • 1. 哈夫曼树的构造过程
    • 2. 哈夫曼算法的实现
      • 2.1 哈夫曼算法思路
      • 2.2 哈夫曼算法实现
  • 三、哈夫曼编码
    • 1. 哈夫曼编码思想
      • 1.1 前缀编码
      • 1.2 哈夫曼编码
      • 1.3 哈夫曼编码的性质
    • 2. 哈夫曼编码的算法实现
    • 3. 文件的编码和解码

前言

编程:将学生的百分之成绩转换成五分制(ABCDE)成绩

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

  • 可以将这样一个判断的过程画成一棵二叉树,每次判断的结果都有两个分支。这样一棵树就称为判断树。

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

假设出现这样一种情况

  • 小于 60 分的同学只有 5%,大于 90 的同学只有 10%。
  • 最多的同学是在 60 -90 之间。

如果每次的输入量很大,则应该考虑程序的操作时间。
若学生的成绩数据有 10000 个:

  • 60-70 分的同学就需要比较两次(先判断是否小于60再判断是否小于70)
  • 同理:70-80 的同学需要比较三次,80-90 要4次,90分以上也是4次。
  • 每个分数线的人数百分比数据总量比较次数,这样的操作量就很大了。
    • 如:处于 60-70 的同学的成绩数据操作数为 15% x 10000 x 2,要比较这么多次,其余同理。

怎样才能让操作次数变少?

如果将判断树设计成这样

  • 先判断成绩是否小于 80 ,小于走左子树,大于则走右子树。

    • 小于 60 分和 60-70 分的数据需要比较三次。
    • 70-80 、 80-90 以及大于 90 的这三种情况都只需要比较两次。
  • 将值判断三次的数据占比相加,将只判断两次的数据占比相加

  • 这种情况总共需要比较:10000(3 * 20% +2 * 80%) = 22000次。

问题

  • 怎样才能找到一种效率最高的判断树呢?
  • 这时候就轮到哈夫曼树(最优二叉树)来干活了。

一、哈夫曼树的基本概念

哈夫曼树又称为最优树,作用是找到一种效率最高的判断树。

  1. 路径:从树中一个结点到另一个结点之间的分支构成这两个结点之间的路径。
  2. 结点的路径长度:两个结点之间路径上的分支数。
    • 如图 a :从 A - D 的路径长度就是是 2。从 A 到 B C D E F G F I 的路径长度分别为 1 1 2 2 3 3 4 4
    • 如图 b:从 A 到 B C D E F G H I 的路径长度分别为 1 1 2 2 2 2 3 3。

  1. 树的路径长度:从树根到每一个结点的路径长度之和称为树的路径长度。

    • 记作:TL(根节点到它自身的结点路径长度为0)
    • 结点数目相同的二叉树中,完全二叉树是路径长度最短的二叉树,但是路径长度最短的不一定是完全二叉树。

  1. 权(weight):将树中结点赋个一个有着某种含义的数值,则称这个数值为该结点的权。

    • 如前言中的那两棵树的每个结点的百分数,权不固定,取什么都可以。
  2. 结点的带权路径长度:从根结点到该结点之间的路径长度与该结点的权的乘积。
    • 如前言中的树中的 小于60分的人数占比 X 到该分支的判断次数( 5 % X 3,)5%是权,3 是路径长度。
  3. 树的带权路径长度:树中所有叶子结点的带权路径之和。

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


  1. 哈夫曼树

    • 最优树:树的带权路径长度(WPL)最短的树。

      • 带权路径长度最短是在树的度相同的树中比较而得到的结果,因此有最优二叉树,最优三叉树之称等等。
    • 最优二叉树:树的带权路径长度(WPL)最短的二叉树。

1. 哈夫曼树的特点

  • 权值越大的结点离根节点越近,权值越小的结点离根节点越远,能使总路径越短。

  • 满二叉树不一定是哈夫曼树。
  • 具有相同带权节点的哈夫曼树不唯一,如上图的 3树和4树,都是哈夫曼树但并不是同一棵树。

二、哈夫曼树的构造算法

在前面知道了哈夫曼树中权越大的叶子离根越近,那么现在我们就要用这样的一个权值来构造哈夫曼树。

贪心算法

  • 构造哈夫曼树时首选选择权值小的叶子结点。
  • 这样就可以将权值大的叶子放到后面构造,大的留在最后就可以离根近一些。

1. 哈夫曼树的构造过程

  1. 根据 n 个给定的权值 {W₁,W₂,W₃…,Wn} 构成 n 棵二叉树的森林 F = {T₁,T₂,T₃…,Tn},其中 Ti 只有一个带权威 Wi 的根节点。

    • 构造的森林全是根。这个森林中,给了几个权值就有几棵树。
  2. 在森林 F 中选取两棵根节点的权值最小的树作为左右子树,构造一棵新的二叉树,且设置新的二叉树的根节点的权值为其左右子树上根结点的权值之和。

    • 选用两小造新树
  3. 在森林 F 中删除这两棵树,同时将新得到的二叉树加入森林中。

    • 删除两小添新人
  4. 重复上面的 步骤2 和 步骤3,直到森林中只有一棵树为止,这棵树即为哈夫曼树。

    • 重复 2、3 剩单根

举个栗子

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

  1. 构造的森林全是根。

  1. 选用两小造新树。

  1. 删除两小添新人。

  1. 重复 2、3 剩单根

因为构造哈夫曼树的时候是,选用两小造新树。

  • 所以:哈夫曼树的结点的度为 0 或 2,没有度为 1 的结点。
  • 包含 n 个叶子结点的哈夫曼树中共有 2n -1 个结点。

总结

  1. 在哈夫曼算法中,初始时有 n 棵二叉树,要经过 n-1 次合并最终成为哈夫曼树。
  2. 经过 n-1 次合并产生 n-1个新结点,且这 n-1 个新结点都是具有两个孩子的分支结点
  3. 可见:哈夫曼树中共有 n+n-1 = 2n-1 个结点,且其所有的分支结点的度均不为1.

2. 哈夫曼算法的实现

  • 采用顺序存储结构—— 一维结构数组。
  • 结点类型定义
typedef struct
{int weight;//结点的权重int parent;//结点的双亲int lchild,rchild;//结点的左、右孩子下标}HTNode,*HuffmanTree;//动态分配数组存储哈夫曼树
  • 哈夫曼树中共有 2n-1 个结点,不使用 0 下标,数组大小为 2n。

    • 例如:第一个结点的权值为 5,即可表示为 H[i].weight = 5

2.1 哈夫曼算法思路

  • 有 8 个权值为 W ={7,19,2,6,32,3,21,10} 的叶子结点,构造哈夫曼树。

  • 这棵哈夫曼树则有 2n-1=2x8-1=15 个结点。

  • 那么现在就应该构造有 2n = 2x8= 16 个元素的数组。


2.2 哈夫曼算法实现

  1. 初始化 HT [1…2n-1]:lchild = rchild = parent = 0;
  2. 输出初始 n 个叶子结点:置为 HT[1…n] 的weight值;
  3. 进行以下 n-1 次合并,依次产生 n-1 个结点 HT[i],i = n+1…2n-1:
    • 在HT[1…i-1] 中选两个未被选过(从parent == 0 的结点中选)的 weight 最小的两个结点 HT[s1] 和 HT[s2],s1、s2为两个最小结点下标。
    • 修改 HT[s1] 和 HT[s2] 的 parent 值:HT[s1].parent = i;HT[s2].parent = i;
    • 修改新产生的 HT[i]:
      • HT[i].weight = HT[s1].weight + HT[s2].weight;
      • HT[i],lchild = s1;HT[i].rchild = s2
//哈夫曼树的存储表示
typedef struct
{int weight;//结点的权重int parent;//结点的双亲int lchild,rchild;//结点的左、右孩子下标}HTNode,*HuffmanTree;//动态分配数组存储哈夫曼树//构造哈夫曼树HT
void CreateHuffmanTree(HuffmanTree &HT,int n)
{if(n<=1){return 0;}int i;int m = 2*n-1HT = new HTNode[m+1];//0号下标未使用,所以需要动态分配m+1(2n)个单元,HT[m]表示根节点for(i = 1;i<=m;i++)//将1-m个元素的双亲、左右孩子的下标都初始化为0{H[i].parent = 0;H[i].lch = 0;H[i].rch = 0;}for(i = 1;i <= n;i++)//输入前n的元素中,叶子结点的权值{cin >> HT[i].weight;}
//-------------------------初始化结束,下面开始创建哈夫曼树-------------------------////通过n-1次的选择、删除、合并来创建哈夫曼树for(i=n+1;i<=m;i++)//从9开始到15结束{select(HT,i-1,s1,s2);//select函数是自己写的//在HT[k](1<=k<=i-1)中选择两个其双亲域为0,且权值最小的结点,并返回他们在 HT 中的序号 s1 和 s2 HT[s1].parent = i;HT[s2].parent = i;//得到新结点i,从森林中删除 s1,s2,将 s1 和 s2 的双亲域由0改1HT[i].lchild = s1;HT[i].rchild = s2;//将s1,s2分别作为 i 的左右孩子HT[i].weight = HT[si].weight + HT[s2].weight;//i的权值为左右孩子的权值之和}
}

三、哈夫曼编码

1. 哈夫曼编码思想

1.1 前缀编码

  • 如果在一个编码方案中,任一字符的编码都不是另一个字符的编码的前缀,这种编码称作前缀编码。

举个栗子

  • 在远程通讯汇总,要将待传字符转换成由二进制的字符换。

  • 设要传送的字符为:ABACCDA,现要将这7个字符转换成由01构成的二进制编码。每个字符都用1个两位的二进制编码表示。

    • 若编码为:A - 00,B - 01,C - 10,D - 11
    • 那么这一串字符就可以转换成 A B A C C D A —> 00 01 00 10 10 11 00
  • 这种编码的方式就称为定长编码,定长编码的缺点就是比较浪费空间,这串字符只有4个不同的字母,但是每个字母都需要占用两个 bit 位。

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

前缀编码的缺点

  • 这个时候我们又发现了一个问题,这串二进制码中有重码,ABA 转换成的 0 使得前面出现了 4 个 0。这样的编码就出现前缀了。
  • 在将二进制翻译成字符的时候就会出现多种情况了。

  • 所以在设计长度不等的编码时,一定不能有重码。

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

    • 这种编码称作前缀编码。

1.2 哈夫曼编码

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

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

  1. 统计字符集中每个字符在电文中出现的平均概率(概率越大,要求编码越短)。
  2. 利用哈夫曼树的特点:权越大的叶子离根越近,将每个字符的概率作为权值,构造哈夫曼树。则概率越大的结点,路径越短。
  3. 在哈夫曼树的每个分支表上 0 或 1:
    • 结点的左分支标 0,右分支标 1。
    • 把从根到每个叶子的路径上的标号连接起来,作为该叶子代表的字符的编码。

举个栗子

【例】要传输的字符集 D = {C,A,S,T,; }(注意这个分号也是,并没有多打),每个字符出现的频率 W = {2,4,2,3,3}。

  1. 以每个字符出现的频率作为权值来构造哈夫曼树:

    • 构造的森林全是根,选用两小造新树,删除两小添新人,重复 2 、3 剩单根。

  1. 将这棵哈夫曼树中所有的左分支标 0,右分支标 1。

  1. 从根节点出发,到每一个叶子结点,将路径上经过的 01 标号连起来。

    • 如:从根到 T 字符,经过了 00 分支,所以 T 的编码就是 00。


哈夫曼编码的两个问题

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

    • 因为没有一片叶子是另一片叶子的祖先,所以每个叶子结点的编码就不可能是其他叶子结点编码的前缀。
  2. 为什么哈夫曼编码能够保证字符编码总长最短?

    • 因为哈夫曼树就是树的带权路径长度最短的树,所以整个字符编码的总长最短。

1.3 哈夫曼编码的性质

  1. 性质1:哈夫曼编码是前缀编码(任一字符的编码都不是另一字符的编码的前缀)。
  2. 性质2:哈夫曼编码是最优前缀码。

举个例子

假设组成电文的字符集 D 及其概率分布 W 为:

  • D = {A,B,C,D,E,F,G}
  • W = {0.40,0.30,0.15,0.05,0.04,0.03,0.03}

设计哈夫曼编码

  • 先构造一棵哈夫曼树。

  • 给所有的左分支标记为 0,给所有的右分支标记为 1。

  • 从根节点到每个叶子结点所经过的标号,就是对应字符的编码。

2. 哈夫曼编码的算法实现

  • 由于从根节点到叶子结点构造哈夫曼编码用算法来实现比较困难,所以在构造哈夫曼树后,求哈夫曼编码的主要思想是:

    • 依次以叶子结点为起点,向上回溯至根节点为止。回溯时如果走左分支则生成代码 0 ,走右分支则生成代码 1。
  • 假设要找到 G 结点的哈夫曼编码。

  1. 查到 G 在数组当中的位置是 7,取出 7 号结点的 parent 域的值,发现是 8 意思就是 8 号结点是它的双亲。
  2. 在 8 号结点的左孩子域找到了 7,说明 7号结点是 8 号的左孩子,是左孩子那么就标注为 0 。
  3. 依次类推直到找到 parent 域为0的结点,就说明G沿着路线走到根节点了。
    • 但是哈弗曼编码是从根到叶子走过的编码,所以要将上述步骤得到的编码翻过来,得到哈夫曼编码。
    • 有 n 个字符上面的步骤就要重复 n 次。

每一个字符都是由若干个 01 组成的哈夫曼编码,所以n 个字符就需要准备 n 个字符串,

由于每个哈夫曼编码是变长编码,因此使用一个指针数组来存放每个字符编码串的首地址。

用一个字符数组来装下 01 编码构成的字符串

  • 有 n 个字符的话,由这 n 个字符所构成的哈夫曼树最高有 n - 1层,因为哈夫曼树一开始肯定是选用两小造新树。
  • 现在有 7 个字符,所以由 01 构成的字符串最长也就是 n -1 = 6,然后数组要多一个元素的 位置来存放 字符串最后的 \0。

算法步骤

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

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

    • 设置变量 start 用于几轮编码在 ca 中存放的位置,start 初始时指向最后一个元素的位置,即 编码结束符 ‘\0’ 的位置。
    • 设置变量 c 用于记录从叶子结点向上回溯至根节点所讲过的结点下标, c 初始时为当前待编码字符的下标 i,f 用于记录 i 的双亲结点的下标;
    • 从叶子结点向上回溯至根结点,求得字符 i 的编码,当 f 没有到达根节点时,循环执行以下操作:
      • 回溯一次 start 向前指一个位置,即 --start;
      • 若结点 c 是 f 的左孩子,则生成代码 0,反之生成代码 1,生成的代码 0 或 1保存在 ca[start] 中。
      • 继续向上回溯,改变 c 和 f 的值。
    • 根据数组 cd 的字符串长度为第 i 个字符编码分配空间 HC[i],然后将数组 cd 中的编码复制到 HC[i] 中。
  3. 释放临时空间 cd 。

哈夫曼算法描述

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

3. 文件的编码和解码

等长编码

  • 如果使用的是之前的等长编码对这段明文进行编译的话,就需要占用 3024bit 的空间。

哈夫曼编码

  • 而用哈夫曼编码则能够节省将近一半的空间。

  • 能通过编码将字符转换成一堆二进制码,同样的也能通过解码将一堆肉眼看着乱七八糟的二进制码转换成给人看的明文。

编码

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

  1. 输入各字符及其权值。
  2. 构造哈夫曼树 —— HT[i]。
  3. 进行哈夫曼编码 —— HC[i]。
  4. 查 HC[i],得到各字符的哈夫曼编码。

解码

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

  1. 构造哈夫曼树。
  2. 依次读入二进制码。
  3. 读入 0,则走向左孩子;读入 1,则走向右孩子。
  4. 一旦到达某个叶子结点时,即可译出对应字符。
  5. 然后再从根出发继续译码,直到结束。

举个栗子

  • 现有如下编码,将它翻译成人话。

  • 接收字符频度表 W:

    • ((u,5),(v,6),(w,2),(x,9),(y,7))
  • 构造哈夫曼树 HT。

  • 从根节点出发,到达每个叶子结点,求出每个字符的哈夫曼编码:u - 110,v - 00,w - 111,x - 10,y - 01。
  • 通过对应字符编码翻译成明文。

【数据结构(25)】5.7 哈夫曼树及其应用相关推荐

  1. 数据结构与算法学习④(哈夫曼树 图 分治回溯和递归)

    数据结构与算法学习④(哈夫曼树 图 回溯和递归 数据结构与算法学习④ 1.哈夫曼树 1.1.相关概念 1.2.哈夫曼树的构建 1.3.哈夫曼编码 1.4.面试题 2.图 2.1.图的相关概念 2.2. ...

  2. 数据结构C#版笔记--啥夫曼树(Huffman Tree)与啥夫曼编码(Huffman Encoding)

    哈夫曼树Huffman tree 又称最优完全二叉树,切入正题之前,先看几个定义 1.路径 Path 简单点讲,路径就是从一个指定节点走到另一个指定节点所经过的分支,比如下图中的红色分支(A-> ...

  3. BJFU_数据结构习题_262基于哈夫曼树的数据压缩算法

    欢迎登录北京林业大学OJ系统 http://www.bjfuacm.com 262基于哈夫曼树的数据压缩算法 描述 输入一串字符串,根据给定的字符串中字符出现的频率建立相应哈夫曼树,构造哈夫曼编码表, ...

  4. Java数据结构和算法:哈夫曼树

    本章介绍哈夫曼树.和以往一样,本文会先对哈夫曼树的理论知识进行简单介绍,然后给出C语言的实现.后续再分别给出C++和Java版本的实现:实现的语言虽不同,但是原理如出一辙,选择其中之一进行了解即可.若 ...

  5. 【数据结构与算法】哈夫曼树的Java实现

    哈夫曼树 最优二叉树也称哈夫曼树,讲的直白点就是每个结点都带权值,我们让大的值离根近.小的值离根远,实现整体权值(带权路径长度)最小化. 哈夫曼算法的思想我认为就是上面讲的,而它的算法实现思路是这样的 ...

  6. 《数据结构与算法之哈夫曼树(Java实现)》

    说在前头: 本人为大二在读学生,书写文章的目的是为了对自己掌握的知识和技术进行一定的记录,同时乐于与大家一起分享,因本人资历尚浅,能力有限,文章难免存在一些错漏之处,还请阅读此文章的大牛们见谅与斧正. ...

  7. 数据结构 --- c语言实现哈夫曼树

    哈夫曼树的结构体描述 #include <stdio.h> #include <stdlib.h> #include <assert.h> #define MAX ...

  8. 【数据结构与算法】-哈夫曼树(Huffman Tree)与哈夫曼编码

    超详细讲解哈夫曼树(Huffman Tree)以及哈夫曼编码的构造原理.方法,并用代码实现. 1哈夫曼树基本概念 路径:从树中一个结点到另一个结点之间的分支构成这两个结点间的路径. 结点的路径长度:两 ...

  9. 数据结构(四)——哈夫曼树的构造(什么时候并列?)

    在判定哈夫曼树的时候,有一个条件就是使其平均比较次数最小,所以依据这个可以判定你构造的哈夫曼树是不是正确的.但是昨天在实践的时候发现,树并列生长的时候不同,导致的结果千差万别,那么 到底什么时候树应该 ...

  10. 数据结构C++语言版 -- 霍夫曼树

    大二学生的 C++数据结构 有部分Openjudge提交的代码没有删除 邮箱:liu_772021@yeah.net 欢迎交流讨论~ 有用的话,点赞留言就可以表示感谢啦 #include <qu ...

最新文章

  1. 排查一般MySQL性能问题
  2. 上周热点回顾(7.10-7.16)
  3. Java当中的IO一
  4. 提高vivado的编译速度
  5. 使用Json让Java和C#沟通的方法
  6. python3.8爬虫_python爬虫系列(3.8-正则的使用)
  7. 将图片序列帧合成mp4_超级详细!如何将B站缓存m4s文件无损转换为mp4格式
  8. python怎么读取csv文件-Python读取csv文件(详解版,看了无师自通)
  9. python怎么读取txt文件-Python三种读取txt文件方式
  10. 关闭文件和打印机共享服务器,网络发现自动关闭、无法启用文件和打印共享的解决办法...
  11. HOUR 13 Developing Advanced References and Pointer
  12. Python.习题八 文件与与异常(上)
  13. mysql通过股票代码查数据_如何在交易数据中查询各个版本交易量前三的股票?(MySQL分组排名)...
  14. MATLAB中的快速傅里叶变换FFT与IFFT
  15. JMockit didn't get initialized
  16. VulnHub日记(八):Hacker Kid
  17. 工字型钢弹性截面模量计算公式_型钢计算公式2
  18. mySQL (关系型数据库管理系统)
  19. 初学ROS--joy包学习
  20. Android利用canvas画各种图形(点、直线、弧、圆、椭圆、文字、矩形、多边形、曲线、圆角矩形)

热门文章

  1. 页面置换算法(FIFO、第二次机会、LRU)
  2. 腾讯云点播html示例文件修改,实现视频居中效果
  3. 微信小程序入门ColorUI组件库使用方法
  4. 4k纸是几厘米乘几厘米_4k纸有多大(4k纸长什么样图片)
  5. hillin:浮木漂流
  6. 2023年湖北初级职称(助理工程师)怎么申报?需要什么材料?启程
  7. Matlab中用Simulink快速画Bode图及 .m 文件画Bode图
  8. 【使用python和flask建个人博客】增加了重复类型的卡片功能,用于更好的完成日常的工作与生活
  9. Python运用循环实现模拟登录
  10. 考过证券从业资格证的朋友们用的什么APP呢?