18.哈夫曼树及其应用

news2024/12/29 10:49:14

目录

一. 基本概念和术语

二. 哈夫曼树的构造

三. 哈夫曼编码


引例:将百分制成绩转换为五级制成绩:<60:E;60-69: D;70-79:C;80-89:B;90-100:A;

一个常用的算法是这样的:

#include <stdio.h>

int main() {
    int score;
    
    printf("请输入百分制成绩: ");
    scanf("%d", &score);
    
    if (score >= 90 && score <= 100) {
        printf("五级制成绩为:优秀\n");
    } else if (score >= 80 && score < 90) {
        printf("五级制成绩为:良好\n");
    } else if (score >= 70 && score < 80) {
        printf("五级制成绩为:中等\n");
    } else if (score >= 60 && score < 70) {
        printf("五级制成绩为:及格\n");
    } else if (score >= 0 && score < 60) {
        printf("五级制成绩为:不及格\n");
    } else {
        printf("输入的成绩无效\n");
    }
    
    return 0;
}

这个决策过程可以画出判断树,判断树是用来描述分类过程的二叉树。如果学生数据量很大,则应该考虑程序操作的时间。现在设共有1万个学生数据,各等级的人数比例在图中标出,则5%的数据需1次比较,15%的数据需2次比较,40%的数据需3次比较,40%的数据需4次比较,因此10000个数据比较的次数为:10000 (1×5%+2×15%+3×40%+4×10%)= 31500次,而左边的决策树只需比较22000次,显然两个决策树的效率不一样,那么我们可否找到一个最优的决策树呢?这就是哈夫曼树(最优二叉树)。

一. 基本概念和术语

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

树的路径长度:从树根到每一个结点的路径长度之和。记作:TL。显然,TL(a)=20,TL(b)=16。

结点数目相同的二叉树中,完全二叉树是路径长度最短的二叉树(但路径长度最短的二叉树不一定是完全二叉树)。

权(weight):将树中结点赋给一个有着某种含义的数值,则这个数值称为该结点的权。
结点的带权路径长度:从根结点到该结点之间的路径长度与该结点的权的乘积。

树的带权路径长度:树中所有叶子结点的带权路径长度之和。记作:WPL=\sum_{k=1}^{n}w_kl_k

哈夫曼树:最优二叉树,带权路径长度(WPL)最短的二叉树。需要注意的是:“带权路径长度最短”是在“度相同”的树中比较而得的结果,因此有最优二叉树、最优三叉树之称等等。
因为构造这种树的算法是由哈夫曼教授于1952年提出的,所以被称为哈夫曼树,相应的算法称为哈夫曼算法。

观察上图能得到下面三个结论:

  • 满二叉树(图1)不一定是哈夫曼树;
  • 哈夫曼树中权值越大的叶子结点离根越近;
  • 具有相同带权结点的哈夫曼树不唯一(图3和图4);

二. 哈夫曼树的构造

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

哈夫曼算法(构造哈夫曼树的方法):
(1)【构造森林全是根】根据n个给定的权值W=\left \{W_1,W_2...W_n \right \}构成n棵二叉树的森林F=\left \{ T_1,T_2...T_n \right \},其中T_i只有一个带权为W_i的根结点。
(2)【选用两小造新树】在F中选取两棵根结点的权值最小的树作为左右子树,构造一棵新的二叉树,且设置新的二叉树的根结点的权值为其左右子树上根结点的权值之和。
(3)【删除两小添新人】在F中删除这两棵树,同时将新得到的二叉树加入森林中。
(4)【重复2,3剩单根】重复(2)和(3),直到森林中只有一棵树为止,这棵树即为哈夫曼树。

例:四个结点的权分别是7,5,2,4,构造哈夫曼树过程如下:

以上构造出来的哈夫曼树结点的度数为0(n个叶子结点)或2(n-1个新生成的结点),没有度为1的结点。如果一开始有n个结点,经过n-1次合并,形成的树结点数是2n-1.

哈夫曼树就是二叉树,下面考虑怎么实现这个算法。实现前我们首先确定哈夫曼树怎么存储,这里采用顺序存储方式,利用一个一维的结构数组(哈夫曼树中共有2n-1个结点,不使用0下标,数组大小为2n)。它的结点类型定义如下:

typedef struct {
    int weight;  //权重
    int parent,lch,rch;  //双亲结点,左孩子结点,右孩子结点
}HTNode,*HuffmanTree;
//HuffmanTree H既是一个指针,也是一个数组,数组返回值就是首元素的地址
//例如,第1个结点权值为5,即可表示为H[i].weight=5;

算法过程如下:

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:
a)在HT[1...i]中选两个未被选过(parent == 0表示未被选过)的weight最小的两个结点HT[s1]和HT[s2],s1、s2为两个最小结点下标;
b)修改HT[s1]和HT[s2]的parent值:HT[s1].parent=i;HT[s2].parent=i;表示两个结点的双亲结点是HT[i];

c)修改新产生的HT[i]:
(新产生结点的权重,为两个子结点权重之和)HT[i].weight=HT[s1].weight+ HT[s2].weight;  
(新产生结点的左右孩子)HT[i].lch=s1;HT[i].rch=s2;

void CreatHuffmanTree (HuffmanTree HT, int n){  //构造哈夫曼树——哈夫曼算法
    if(n<=1) return ERROR;
    m=2*n-1;  //数组共2n-1个元素
    HT=new HTNode[m+1];  //构造长2n的数组
    for(i=1;i<=m;++i){  //将2n-1个元素的lch、rch、parent置为0
        HT[i].Ilch=0; 
        HT[i].rch=0; 
        HT[i].parent=0;
    }
    for(i=1;i<=n;++i) cin>>HT[i].weight;  //输入前n个元素的weight值
    //初始化结束,下面开始建立哈夫曼树
    for(i=n+1;i<=m;i++){  //合并产生n-1个结点——构造Huffman树
    Select(HT,i-1,s1,s2);  //在HT[k](1≤k<i-1)中选择两个其双亲域为0,
    //且权值最小的结点,并返回它们在HT中的序号s1和s2
    //这个函数需要另行实现
    HT[s1].parent=i; 
    HT[s2].parent=i;  //表示从F中删除s1,s2
    HT[i].lch=s1;
    HT[i].rch=s2;  //s1,s2分别作为i的左右孩子
    HT[i].weight=HT[s1].weight + HT[s2].weight;  //i的权值为左右孩子权值之和
    }
}

三. 哈夫曼编码

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

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

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

什么样的前缀码能使电文总长最短?——哈夫曼编码

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

例:要传输的字符集D={C,A,S,T,;},字符出现的频率w={2,4,2,3,3}。

利用上述编码,电文是{CAS;CAT;SAT;AT},其编码是:11010111011101000011111000011000”,反之,若编码是1101000,对应CAT。

两个问题:

  • 为什么哈夫曼编码能够保证是前缀编码?——因为没有一片树叶是另一片树叶的祖先,所以每个叶结点的编码就不可能是其它叶结点编码的前缀;
  • 为什么哈夫曼编码能够保证字符编码总长最短?——因为哈夫曼树的带权路径长度最短,故字符编码的总长最短。

关于编码实现:首先根据字符和权值得到哈夫曼树,然后逆序寻找。

例如,寻找G的哈夫曼编码,G在HT[7]中,HT[7]的parent是8,查HT[8]可知HT[7]是HT[8]的左孩子,所以哈夫曼编码的最后一位是0;然后再去查HT[8]的双亲结点,发现是HT[10],并且HT[8]是HT[10]的左孩子,所以哈夫曼编码的倒数第二位是0...以此类推,直到找到HT[12]的双亲结点是HT[13],且HT[13]的parent是0,这表示我们已经找到根结点,并且HT[12]是HT[13]的右孩子,说明哈夫曼编码的第一位是1。然后我们根据输出顺序00001倒过来就是G的哈夫曼编码值10000。

在代码实现中,HT数组存放哈夫曼树,HC数组存放每个字符的哈夫曼编码,即哈夫曼编码表;同时还有一个cd数组用来临时存放输出的每一位哈夫曼编码数值,并倒序输出(这里笔者认为直接用栈实现会更好)。n个字符下,哈夫曼树最多有n-1层,所以cd数组的长度是n,最后一位存放'\0'表示字符结束标志。

void CreatHuffmanCode(HuffmanTree HT,HuffmanCode &HC,int n){ 
//从叶子到根逆向求每个字符的哈夫曼编码,存储在编码表HC中
    HC = new char *[n+1];  //分配n个字符编码的头指针矢量,0号位空出,HC是指针数组,存放字符数组的地址
    cd = new char [n];  //分配临时存放编码的动态数组空间
    cd[n-1] ='\0';  //编码结束符
    for(i=1; i<=n; ++i){  //逐个字符求哈夫曼编码
        start = n-1;  //cd数组的索引
        c = i;  //临时存放结点在HT中的下标
        f = HT[i].parent;
        while(f!=0){  //从叶子结点开始向上回溯,直到根结点
            --start;  //回溯一次start向前指一个位置
            if(HT[f].lchild == c) 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;  //释放临时空间
}  //CreatHuffanCode

上面讨论了生成哈夫曼编码,下面考虑解码:

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/918516.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

android外卖点餐界面(期末作业)

效果展示&#xff1a; AndroidMainFest.xml <?xml version"1.0" encoding"utf-8"?> <manifest xmlns:android"http://schemas.android.com/apk/res/android"xmlns:tools"http://schemas.android.com/tools"><a…

SpringBoot返回响应排除为 null 的字段

SpringBoot返回响应排除为 null 的字段 可以通过全局配置&#xff0c;使返回响应中为null的字段&#xff0c;不在出现在返回结果中。 注意&#xff1a;这样配置&#xff0c;使得返回响应包含的字段随请求结果变化&#xff0c;响应到底包含哪些字段不直观&#xff1b;除非业务…

[JavaWeb]【十】web后端开发-SpringBootWeb案例(配置文件)

目录 一、参数配置化 1.1 问题分析 1.2 问题解决&#xff08;application.properties&#xff09; 1.2.1 application.properties 1.2.2 AliOSSUtils 1.2.3 启动服务-测试 二、yml配置文件 2.1 配置格式 2.1.1 新增 application.yml 2.1.2 启动服务 2.2 XML与prope…

虚拟化技术小结

CPU时分复用原理 虚拟化本质 对底层硬件资源的复用&#xff0c;技术原理就是时分复用实现的。 原理 前提 1.CPU有很多核心&#xff0c;即core。CPU每个core同时且只能执行一个进程。 2.CPU&#xff08;core&#xff09;执行的时间可以被切分任意大小的时间片&#xff0c…

企业展示小程序的制作流程及关键步骤详解

在移动互联网时代&#xff0c;企业展示小程序已成为各个行业推广和展示的重要工具。搭建一个企业展示小程序不仅能够提高企业形象&#xff0c;还能够增加用户粘性和提升用户体验。下面我们来看一下如何从零基础搭建一个企业展示小程序&#xff0c;并顺利上线。 第一步&#xff…

【Java 高阶】一文精通 Spring MVC - 数据验证(七)

&#x1f449;博主介绍&#xff1a; 博主从事应用安全和大数据领域&#xff0c;有8年研发经验&#xff0c;5年面试官经验&#xff0c;Java技术专家&#xff0c;WEB架构师&#xff0c;阿里云专家博主&#xff0c;华为云云享专家&#xff0c;51CTO 专家博主 ⛪️ 个人社区&#x…

谈谈对 GMP 的简单认识

犹记得最开始学习 golang 的时候&#xff0c;大佬们分享 GMP 模型的时候&#xff0c;总感觉云里雾里&#xff0c;听了半天&#xff0c;并没有一个很清晰的概念&#xff0c;不知 xmd 是否会有这样的体会 虽然 golang 入门很简单&#xff0c;但是对于理解 golang 的设计思想和原…

【0823作业】C++:实现类嵌套,以及其构造函数、析构函数和拷贝构造函数

要求&#xff1a; 设计一个Per类。类中包含私有成员&#xff1a;姓名、年龄、指针成员身高、体重&#xff1b; 再设计一个Stu类&#xff0c;类中包含私有成员&#xff1a;成绩、Per类对象 p1&#xff1b; 设计这两个类的构造函数、析构函数和拷贝构造函数。 #include <iostr…

spring之Spring Boot入门与快速启动

Spring Boot入门与快速启动 摘要:引言:词汇解释:详细介绍:什么是Spring Boot以及其特点: 什么是Spring Boot以及其特点Spring Boot 简介:Spring Boot 的特点: 注意事项:使用Spring Boot初始化项目:使用 Spring Initializr:使用 Spring Boot CLI: 注意事项:代码示例:自动配置和约…

【数据库】详解数据库架构优化思路(两主架构、主从复制、冷热分离)

文章目录 1、为什么对数据库做优化2、双主架构双主架构的工作方式如下&#xff1a;双主架构的优势包括&#xff1a;但是一般不用这种架构&#xff0c;原因是&#xff1a; 3、主从复制主从复制的工作方式如下&#xff1a;主从复制的优势包括&#xff1a;主从复制的缺点 4、冷热分…

我是怎么从0到1搭建性能门禁系统的

背景 页面的性能对于用户的体验起着至关重要的作用&#xff0c;根据Mobify 研究发现&#xff0c;首页加载时间每减少100 毫秒&#xff0c;用户留存率就会增加1.11%。所以做好页面的性能优化&#xff0c;对于网站来说是一个非常重要的步骤。 在解决问题之前需要度量问题&#x…

吴师傅教你怎样开启联想电脑管家的极速模式

如果你的笔记本出现卡顿的情况&#xff0c;可以在联想电脑管家里开启极速模式试一下&#xff0c;会有运行速度上的提升&#xff0c;具体方法如下&#xff1a; 1、双击打开桌面上的联想电脑管家&#xff1b; 2、在打开的“联想电脑管家”界面里&#xff0c;点击右上边的“实用工…

mathematica 提取Solve(NSolve)函数变量

直接上例子&#xff0c;非常直观 求解的方程是&#xff1a; 0.7 sin ⁡ ( x ) 0.7 sin ⁡ ( 2 x ) 0.6047 0.7 \sin (x)0.7 \sin (2 x)0.6047 0.7sin(x)0.7sin(2x)0.6047 提取Solve(NSolve)函数变量&#xff0c;列表提取第一个元素 列表提取第3个元素 提取第三个元素的…

timer定时器,使用timer定时器完成LED123点亮

使用timer定时器完成LED123&#xff0c;一秒亮&#xff0c;一秒灭 #include "head.h" #include <linux/init.h> #include <linux/module.h> #include <linux/fs.h> #include <linux/uaccess.h> #include <linux/io.h> #include <l…

存在逻辑删除的表字段上建立唯一索引的巧办法 (逻辑删除与唯一索引)

存在逻辑删除的表字段上建立唯一索引的巧办法 首先&#xff0c;我们肯定是清楚地知道唯一键值逻辑删除位如果联合创建唯一索引的话&#xff0c;只能最多存在两条数据&#xff0c;无法满足不断删除新增的需求&#xff0c;所以需要一个巧妙的办法去实现有逻辑删除标志位的唯一索…

Anaconda安装pytorch-cuda

1 查看电脑对应cuda版本 【win-R】> 【cmd】> 确定 执行nvidia-smi&#xff0c;可以从图中看出&#xff0c;该电脑的CUDA Version为&#xff1a;11.6 2 官网查找对应的版本的安装语句 PyTorch官网&#xff1a;https://pytorch.org/ 2.1 可以直观的选择对应的版本 如…

LabVIEW开发聋哑人智能辅助手套

LabVIEW开发聋哑人智能辅助手套 今天的残疾人面临着许多挑战&#xff0c;最大的问题是沟通效率低下。由于这些人通常使用通信来请求基本必需品&#xff0c;因此在他们的日常生活中起着重要作用。能力不同的人通常会找到解决这个问题的方法。残疾人很难表达自己的需求&#xff…

MyBatis的场景应用(动态SQL、模糊查询及映射结果)附(Mybatis中#和$的区别)

一.Mybatis简介 MyBatis&#xff08;之前被称为iBatis&#xff09;是一种开源的持久化框架&#xff0c;它将面向关系数据库的持久层操作封装起来&#xff0c;使得开发人员可以通过简单的配置来实现对数据库的操作。MyBatis提供了灵活且强大的SQL映射功能&#xff0c;能够将数据…

关于融合项目点云pointpillars检测不显示三维检测框问题的解决

这个问题主要还是launch文件中出现了一些偏差。 launch文件的第26行 这里原先是0.6&#xff0c;在检测kitti的时候是0.6&#xff0c;由于kitti是64线激光雷达&#xff0c;我个人用的是16线激光雷达&#xff0c;所以把0.6降到了0.2.出现了三维检测框&#xff0c;问题解决

Linux 安装mysql(ARM架构)

添加mysql用户组和mysql用户 安装依赖libaio yum install -y libaio* 下载Mysql wget https://obs.cn-north-4.myhuaweicloud.com/obs-mirror-ftp4/database/mysql-5.7.27-aarch64.tar.gz安装mysql 解压Mysql tar xvf mysql-5.7.27-aarch64.tar.gz -C /usr/local/ 重命名 …