数据结构(霍夫曼树)

1. Huffman编码

1.1 问题起源

假设在数据通信中,有一字串"ABABBCBBA"需要传送,一般会将这些字符进行编码,然后按编码后的二进制位进行传输,例如这些字母的ASCII码取值为:

A(65): 0100 0001

B(66): 0100 0010

C(67): 0100 0011

因此最"简单"的方式,就是将上述字串直接使用字符编码方式转换成如下01序列进行传输(总共72位):

复制代码
01000001 01000010 01000001 01000010 01000010 01000011 01000010 01000010 01000001

1

很容易发现,上述字串中,虽然每个字母只用了一个字节来编码,但是每个字节的前面6位都是"0100 00",因此完全可以将这无效编码去掉,变成:

A: 01

B: 10

C: 11

这样一来,字串"ABABBCBBA"编码后就变成了:

复制代码
0110 0110 1011 1010 01

整个编码长度缩减为18位。但这并未达到压缩的极致,仔细观察上述ABC三个字符的编码,会发现其实把A的编码从01换成0也可以,这又省掉了一位。字串"ABABBCBBA"编码后就变成了:

复制代码
0100 1010 1110 100

二进制编码进一步缩短为15位。这里有一点要着重注意,将A的编码从01换成0是可以的,但是不能换成1,因为B和C的编码都是从1开始的,如果A的编码是1的话,在译码阶段就会出现二义性,比如当出现"11011..."时,就无法区分第一个1究竟是A还是跟后面的1结合形成C。

经过观察又会发现,字串中B出现的概率比A多,因此我们完全可以将性价比更高(即更短)的编码给B,变成:

A: 10

B: 0

C: 11

这样一来,字串"ABABBCBBA"编码后就进一步变成了:

复制代码
1001 0001 1001 0

1

此时,字串的编码只有13位。并且注意到,由于做了谨慎的处理,逐个读取二进制位译码时,不会发生二义性。

1.2 Huffman编码

在上面的分析中,那种基于字符出现的频次,按频次给不同给不同字符分配长短不一的编码方式,就是Huffman编码的基本思路:

  1. 找出字串中各个字符的出现频次,如:[A3,B5,C1][A 3,B 5,C1]。
  2. 以频次作为叶子节点的权值,构造一棵Huffman树。
  3. 将Huffman树中所有的左子树的边标记为0,右子树的边标记为1,则每一个叶子节点的路径就是Huffman编码的数值。

以上述字串"ABABBCBBA"为例,这三个节点的频次构成的Huffman树是:

在此Huffman树中,按照上述编码的思路,将左子树路径标记为0,右子树路径标记为1:

那么[A3,B5,C1][A 3,B 5,C1]的路径分别是:

A3A 3: 10

B5B 5: 0

C1C1: 11

这刚好就是上述描述的最优长短编码。下面,来看Huffman树是怎么构建的。

2. Huffman树

2.1 基本概念

Huffman树一般称为霍夫曼树,又称为最优二叉树,为什么是最优呢?以下面的二叉树为例,先弄清几个概念:


  • 有时会用一个数值来表征一个节点,比如上述每个节点中的数字。这些权值具体到实际应用当中可以表达一种程度、某个标值等,比如Huffman编码中会提到的某个字符出现的频率。
  • 路径及路径的长度
    从根节点开始,到叶子节点的一条通路,比如上图中红色虚线所示的路径:3、2、8、13、2、8、1
    路径所包含的边的数目,称为路径的长度,比如上述从根节点3到叶子节点1的路径中,包含了3条边,因此该路径的长度为3。
  • 带权的路径长度
    如果在计算路径长度时,考虑到达某个节点所经过的边数目,将这些数据累加起来作为路径长度的话,这样的路径长度称为带权的路径长度。
    比如上述路径3、2、8、13、2、8、1,其带权路径是:

3∗0+2∗1+8∗2+1∗3=2+16+3=213∗0+2∗1+8∗2+1∗3=2+16+3=21

  • 树的带权路径长度
    容易理解,树的带权路径长度就是所有的叶子节点的带权路径长度之和,称为WPL (即W eighted P ath Length)。还是以上述二叉树为例,此处WPL应等于:

(1∗2+2∗6)+(1∗2+2∗8+3∗1)+(1∗5)=14+21+5=41(1∗2+2∗6)+(1∗2+2∗8+3∗1)+(1∗5)=14+21+5=41

很明显,如果变换一下各个带权节点的位置,则可以改变整棵树的带权路径的长度,例如做如下变换,让权值大的节点尽量往根部靠近(以减少路径长度):

此时树的带权路径长度为:

(1∗6+2∗1)+(1∗5+2∗2)+(1∗5+2∗3)=8+9+11=28(1∗6+2∗1)+(1∗5+2∗2)+(1∗5+2∗3)=8+9+11=28

2.2 Huffman树的定义

给定N个权值为N的叶子节点,构建一棵树,如果这棵树的WPL达到最小值,就称这样的树为最优树,或霍夫曼树。

如果构建的树是二叉树,那么这样的二叉树称为最优二叉树。

2.3 Huffman树的节点数量

抛出这么一个问题:当待编码的字符有m个时,最终构建出来的霍夫曼树的节点数总共有几个?

当一棵霍夫曼树的叶子节点数 n0=3n 0​=3 (此处的n0n0​就是上述代码中的m)时,整棵霍夫曼树的节点树是多少呢?答案是5。下面是简单的推导过程:

  1. 假设二叉树节点总数为nn ,度为0的节点数为n0n 0,度为1的节点数为n1n 1,度为2的节点数为n2n2,那么显然有:

n=n0+n1+n2n =n 0+n 1+n2

  1. 从另一个角度考察,二叉树的节点总数也等于所有节点的子节点个数之和:n0n 0节点的子节点的总数是0∗n00∗n 0,n1n 1节点的子节点总数是1∗n11∗n 1,n2n 2节点的子节点总数是2∗n22∗n2,又由于根节点不是任何节点的子节点,因此有:

n=0∗n0+1∗n1+2∗n2+1=n1+2∗n2+1n =0∗n 0+1∗n 1+2∗n 2+1=n 1+2∗n2+1

  1. 结合上述两个式子得到:

n2=n0−1n 2=n0−1

  1. 霍夫曼树中,所有的权值节点均是叶子结点,且不存在度为1的节点,即n1=0n1=0,因此有:

n=n0+n1+n2=n0+n0−1=2∗n0−1n =n 0+n 1+n 2=n 0+n 0−1=2∗n0−1

结论:

当权值节点数量为n0n 0​时,霍夫曼树的节点总数为2∗n0−12∗n0​−1

2.4 Huffman树构建的基本思路

从以上带权路径最小化基本思路出发,可以构建一棵Huffman树。仍然以字串"ABABBCBBA"为例,基本思路是:

  1. 计算各个字符所出现的频次:

"ABABBCBBA"

A:出现3次

B:出现5次

C:出现1次

不同字符的数量是:3

  1. 将这些频次作为节点的权值,放在一个数组中:

    int m = 3; // 3是子串所出现不同的字符数量[A, B, C]
    int huffmanTree[2*m-1] = {3, 5, 1, 0, 0}; // 此处伪代码,不可编译

这些权值节点就是霍夫曼树的叶子节点,如下图所示:

权值节点

  1. 从这些叶子节点出发,逐步构建整棵霍夫曼树:在已知的叶子中找到两个最小且无父节点的叶子,接他们权值相加并将和放入后续数组中,比如找到3和1之后,生长出节点4:

    huffmanTree[] = {3, 5, 1, 4, 0};

在上述操作过程中,关键是要标注节点之间的关系,必须指明3和1是4的子节点,4是它们的父节点,这样,在下一轮构建中3和1将不再参与,最终构建出霍夫曼树:

复制代码
huffmanTree[] = {3, 5, 1, 4, 9};
  1. 从叶子节点出发,向上寻找根节点,将沿途经历过的路径加以编码(比如左路径为0、右路径为1),再反转,得到的就是改节点的霍夫曼编码,比如:从节点3开始,到根节点9,其经过的路径编码是:01,因此权值为3的节点A霍夫曼编码就是10。最终得到:

A3,B5,C1\]\[*A* 3,*B* 5,*C*1\]的路径分别是: > A3*A* 3: 10 > > B5*B* 5: 0 > > C1*C*1: 11 #### **2.5 Huffman树的构建步骤** * 【步骤1】准备一个存储空间为 2∗m−12∗*m* −1 (即2∗n0−12∗*n*0−1)的数组来存储霍夫曼树: // 霍夫曼树节点 typedef struct { char ch; // 字符 int weight; // 权重,即字符出现的频次 int parent; // 父节点位置 int lchild; // 左子树位置 int rchild; // 右子树位置 }huffmanTreeNode; // 统计字符串s中的各个字符 void initHuffmanTree(const char *s, huffmanTreeNode **ht, int *pm) { // 统计s中不同字符的个数,放入*pm中 // 以及各个字符出现的频次,放入alphabet中 *pm = 0; int alphabet[26] = {[0 ... 25]=0}; for(int i=0; s[i]!='\0'; i++) alphabet[s[i]-'A']++; for(int i=0; i<26; i++) { if(alphabet[i] != 0) (*pm)++; } // 存储各个字符出现的频次 // 所需的霍夫曼节点总数n = 2*m - 1 // [a1, a2, a3, ..., am, 0, 0, ... , 0] // | <-- m个字符 --> | *ht = calloc((*pm)*2-1, sizeof(huffmanTreeNode)); for(int i=0,k=0; i<26; i++) { if(alphabet[i] != 0) { (*ht)[k].ch = 'A' + i; (*ht)[k].weight = alphabet[i]; k++; } } } ![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/fd10a1436f164fb5a20844d7910815b2.png) * 【步骤2】根据权值节点数组,构建霍夫曼树: // 函数功能:在数组ht中找到两个最小值,分别用p1和p2标识其下标 // 参数解析: // ht:霍夫曼树数组 // end:数组边界下标 // p1与p2:最小值位置下标 void find(huffmanTreeNode *ht, int end, int *p1, int *p2) { int min1 = 0; int min2 = 0; for(int i=0; i<=end; i++) { if(ht[i].parent != 0) continue; if(min1 == 0) { min1 = ht[i].weight; *p1 = i; continue; } else if(min2 == 0) { if(min1 < ht[i].weight) { min2 = ht[i].weight; *p2 = i; } else { min2 = min1; min1 = ht[i].weight; *p2 = *p1; *p1 = i; } continue; } if(ht[i].weight < min1) { min1 = ht[i].weight; *p2 = *p1; *p1 = i; } else if(ht[i].weight < min2) { min2 = ht[i].weight; *p2 = i; } } } void createHuffmanTree(huffmanTreeNode *ht, int m) { // 从叶子开始,构建霍夫曼树 int p1, p2; for(int i=m; i<2*m-1; i++) { // 在ht中找到两个最小且未有父节点的节点 // 并将其用p1与p2标识出来 find(ht, i-1, &p1, &p2); ht[p1].parent = i; ht[p2].parent = i; ht[i].weight = ht[*p1].weight + ht[*p2].weight; ht[i].lchild = *p1; ht[i].rchild = *p2; } } 至此,一棵Huffman树ht就创建完毕了,有了ht之后,可以设计一个函数来展示从外部命令行输入的字串对应的编码: #### **2.6 Huffman码表** 由上述1.2小节知道,如果有了一棵Huffman树,那么叶子节点的编码就是从叶子到根的路径的翻转,比如下图: ![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/9c4cbbb7f843405591f7efe7ad8dec3d.png) 上图中,如果约定左子树为0,右子树为1,那么叶子节点3到根的路径就是01,那么最终叶子节点3的编码就是10。 下述代码,实现输出ht中第i个节点的Huffman编码值: void huffmanCode(huffmanTreeNode *ht, int i) { char hcode[20]; int k; for(k=0; ht[i].parent != 0; k++) { if(ht[ht[i].parent].lchild == i) hcode[k] = '0'; else if(ht[ht[i].parent].rchild == i) hcode[k] = '1'; i = ht[i].parent; } for(int m=k-1; m>=0; m--) printf("%c", hcode[m]); printf("\n"); } 将以上各个代码模块整合起来,可以得到Huffman编码的Demo程序: int main(int argc, char const *argv[]) { int m; huffmanTreeNode *ht; // 初始化一棵霍夫曼树,将已统计的频次作为叶子 // 放入树的前m项中:[3, 5, 1, 0, 0, ... , 0] initHuffmanTree(argv[1], &ht, &m); // 从叶子开始,构建霍夫曼树 createHuffmanTree(ht, m); // 根据已构建的霍夫曼树,生成霍夫曼编码表 printf("霍夫曼编码表:\n"); for(int i=0; i

相关推荐
跟着珅聪学java1 小时前
spring boot +Elment UI 上传文件教程
java·spring boot·后端·ui·elementui·vue
我命由我123451 小时前
Spring Boot 自定义日志打印(日志级别、logback-spring.xml 文件、自定义日志打印解读)
java·开发语言·jvm·spring boot·spring·java-ee·logback
lilye661 小时前
程序化广告行业(55/89):DMP与DSP对接及数据统计原理剖析
java·服务器·前端
战族狼魂5 小时前
CSGO 皮肤交易平台后端 (Spring Boot) 代码结构与示例
java·spring boot·后端
xyliiiiiL6 小时前
ZGC初步了解
java·jvm·算法
杉之6 小时前
常见前端GET请求以及对应的Spring后端接收接口写法
java·前端·后端·spring·vue
hycccccch7 小时前
Canal+RabbitMQ实现MySQL数据增量同步
java·数据库·后端·rabbitmq
天天向上杰8 小时前
面基JavaEE银行金融业务逻辑层处理金融数据类型BigDecimal
java·bigdecimal
请来次降维打击!!!8 小时前
优选算法系列(5.位运算)
java·前端·c++·算法
用键盘当武器的秋刀鱼8 小时前
springBoot统一响应类型3.5.1版本
java·spring boot·后端