贪婪算法(Huffman编码)

news2024/12/28 5:03:09

如果一个算法分阶段的工作,并且在每一个阶段都认为所做的决定是最好的,而不考虑将来的后果,这样的算法就叫做贪婪算法。贪婪算法只考虑当前局部的最优解,而不去考虑全局的情况,如果最终得到的结果是全局最优的,那么算法就是正确的,否则贪婪算法就只能得到一个次优解。如果不要求得到绝对的答案,那么使用贪婪算法得到次优解也是可以的,否则就需要使用更复杂的算法来得到准确结果。

举一个例子来说明贪婪算法不一定能够得到最优解:如果有12,10,5,1四种面值的纸币,要用最少的纸币来得到16元,如果使用贪婪算法,那么第一张选择12,还需要选择4张1元,这就需要5张纸币,但使用一张10,一张5以及一张1,就只需要三张纸币,这就说明贪婪算法并不总是成功的。

调度问题:

单处理器:

贪婪算法的一个例子是调度问题。现在有作业j_{1},j_{2},j_3,j_4四份作业,完成这四份作业的时间分别为t_1,t_2,t_3,t_4,只有一个处理器,并且使用非预占调度:一旦开始一个作业,就必须把当前的作业运行完。如果要使完成作业的平均时间最短,应该如何调度?

作业时间
j_115
j_28
j_33
j_410

k表示处理的顺序,i_k表示作业的编号,那么处理完这四份作业所需要的总时间(即每个作业从处理器开始工作到完成该作业所花费的时间总和):

T=

T=

在第二个方程中,可以看到,等式后面的第一项是与作业处理顺序无关的,而要使T最小,第二项就应该尽可能的大 ,那么由数学中的排序不等式可知,当kt_{i_k}为同序时,也就是花费时间少的作业,处理的顺序也应该靠前,这样就能使第二项达到最大,从而使总时间达到最小。所以最优调度方案如图:

多处理器:

将问题推广到多处理器,假设有9份作业和3个处理器,那么最优调度方案其实与单处理器相似,只需要将9份作业从小到大排序,依次交由三个处理器处理即可(即使作业的份数不能整除处理器的个数,最优解依然是存在的):

工作时间
j_13
j_25
j_36
j_410
j_511
j_614
j_715
j_818
j_920

但多个处理器在平均时间最优的情况下会有一个最终结束时间问题,如果交换7,8,9三份作业的位置,如下图:

那么可以看到,第二个方案的最终结束时间是38,而第一个方案是40。文中的这个例子存在一个最优的结束时间:

虽然对于单个处理器来说,完成自身所有任务的平均时间可能并没有前两个方案好,但是如果在实际中,我们需要等待这些任务处理完成后,再去进行下一步的操作,那么这时候最短的结束时间就是很有必要的。

Huffman编码:

贪婪算法的另一个例子就是文件压缩问题。

对于一个需要传输的文件,如果将文件中的所有字符都以ASCII码值编码,那么每个字符就需要8个bit,在文件比较大并且带宽较小的情况下,这个文件的传输将会非常的耗时。实际上,ASCII码值占8个bit是由于:标准的ASCII字符集由100个可打印字符及一些非打印字符组成,1个bit能表示两种情况,那么7个bit就能够表示128种不同的字符,再加上最高位作为奇偶校验位(校验数据传输是否正确),总共8个bit。

那么,如果一个文件中出现的不同字符总共有N个,实际上每个字符就需要\left \lceil log_{2}N \right \rceil个比特位来表示,在这种编码下,文件就被缩小了。

假设有一个文件,包含字符a、e、i、s、t,再加上一些空格和换行,并且有10个a、15个e、12个i、3个s、4个t、13个空格和1个换行,如图所示,这个文件就需要174位来表示:

字符二进制编码频率总位数
a0001030
e0011545
i0101236
s01139
t100412
空格1011339
换行11013
总和174

这个编码值可以使用一棵二叉树来表示:

在树中,左边表示0,右边表示1,每一个叶节点都代表一个字符,这样就可以保证编码无冲突,从根节点找到一个叶节点所经过的路径,就代表了这个叶节点字符的编码值。观察这幅图可以发现,换行符的父节点只有它一个孩子,所以可以将换行符放到它父节点的位置:

此时,换行符的编码就变成了11,则文件的总位数也变成了173,节省了一个bit的长度。上图中的树称为满树:所有的节点要么是叶子节点,要么有两个子节点。一些最优的编码总是具有这个性质,否则根据刚才的操作,我们总可以将只有一个孩子的节点向上移动一层 。

如果字符只放在叶子节点上,那么所有的字符就总是能被无歧义的编码。否则,假设o的编码是00,那么o的位置如图:

对于二进制码000001,我们就不能够知道它代表的是 ae 还是 oo或是其他,这样在编码时就会产生歧义,因为o是a的前缀编码。所以只有字符都放在叶子节点上,那么它们就不可能是任何其他字符的前缀编码,自然也就不会产生歧义。在文件中,e出现了最多次,那么如果我们将nl与e的位置互换,最后文件就只有159位,这就是Huffman算法的思想。

Huffman算法:

由Huffman算法生成的树就叫作Huffman树,得到的编码就是Huffman编码。Huffman算法的基本思想就是:对于一个文件中出现的所有字符,首先将其建立为一个森林,每个节点都存放字符本身以及字符出现的频率,然后选择其中频率最小的两个节点合并为一棵树,这棵树的频率为合并的两个节点频率树之和,然后将合并后的树放回森林,再找到两个频率最小的节点重复上述工作,直到最后只剩一棵树,就完成了Huffman编码。对于上述文件,建立Huffman树的具体过程如下:

首先建立初始森林:

选择频率最小的两个节点nl、s进行合并:

 然后重复操作:

 

 

 

 

到这里就完成了Huffman树的建立。

Huffman树一定是一棵满树,因为如果不是满树,就一定存在一个只有一个孩子的节点,那么在合并时,就一定是只将森林中的一个节点与一个NULL进行合并,这与我们的操作过程是不符的;频率最小的节点一定是最深的,由数学归纳法知,在第一次合并时,显然结论是成立的。假设在T3次合并时结论也成立,那么在T5次合并时,如果e的频率小于T3的频率 ,并且e的频率要小于a或是T2的频率,那么在T3合并时,就会先将e进行合并,所以e的频率一定是大于比它更深的节点的。这样就可以使出现次数最多的字符的编码值是最短的,得到的编码就是最优的。

Huffman算法是一个贪婪算法是因为,在每次合并时,都是选择当前频率最小的两个节点进行合并,而并没有进行全局的考虑。如果使用堆来找到节点中频率最小的两个节点,那么Huffman算法的时间复杂度为O(NlogN)N为不同字符的个数。如果使用插入排序的话,时间复杂度就为O(N^2)。将N个节点合并成一棵树需要线性时间,所以时间主要花费在排序上。

有两个细节需要考虑:

一是在传输压缩文件时,必须要传送编码信息,否则将无法解码,如果文件本身并不大,那么传输编码信息的代价可能超过了编码所带来的节省,这个时候不对文件进行压缩可能是一个更好的选择。

二是我们首先要直到文件中每个字符的频率,然后才能对它们使用Huffman算法,如果文件过大,那么就需要慎重考虑得到字符频率的算法。

Huffman算法代码如下:

typedef struct TreeNode {//树的节点,存放字符、频率以及子节点信息
	char ch;
	int times;
	struct TreeNode* nodes[2];//向左为0,向右为1,正好以数组下标来表示
}TreeNode;

typedef struct HuffmanTree {//Huffman树,存放树的根节点
	TreeNode* root;
}HuffmanTree;

void insertion_sort(TreeNode** p, int n) {//插入排序
	for (int i = 1; i < n; i++) {
		TreeNode* tmp = p[i];
		int j;
		for (j = i; j >= 1 && p[j - 1]->times < tmp->times; j--) {
			p[j] = p[j - 1];
		}
		p[j] = tmp;
	}
}

HuffmanTree* BuildHuffmanTree(TreeNode* arr, int n) {//建立一个Huffman树
	HuffmanTree* p = (HuffmanTree*)malloc(sizeof(HuffmanTree));
	p->root = NULL;
	TreeNode** pt = (TreeNode**)malloc(sizeof(TreeNode*) * n);//建立一个森林
	for (int i = 0; i < n; i++) {//将每个字符的数据都建立成一个树节点
		TreeNode* ptt = (TreeNode*)malloc(sizeof(TreeNode));
		ptt->ch = arr[i].ch;
		ptt->nodes[0] = ptt->nodes[1] = NULL;
		ptt->times = arr[i].times;
		pt[i] = ptt;
	}

	insertion_sort(pt, n);//对森林进行排序,从大到小

	for (int i = n - 1; i >= 1; i--) {
		TreeNode* ptt = (TreeNode*)malloc(sizeof(TreeNode));
		ptt->nodes[0] = pt[i];//选择最小的两个节点,并将它们合并
		ptt->nodes[1] = pt[i-1];
		ptt->times = pt[i]->times + pt[i - 1]->times;
		pt[i - 1] = ptt;//每合并两个节点,森林中的节点数就会-1,所以将合并后的节点放在pt[i-1]位置,并重新排序
		insertion_sort(pt, i);
	}

	p->root = pt[0];//当最后合并的节点放在pt[0]处时,说明森林中只剩一棵树,那么这个节点就是Huffman树的根节点

	return p;//返回根节点
}

void print(TreeNode* p, char* buff, int i) {
	if (p->nodes[0] == NULL && p->nodes[1] == NULL) {//如果左右子树都为空,说明这个就是字符所在的叶节点
		buff[i] = 0;//那么buff就是该字符的编码值,所以直接输出
		printf("%c   %s\n", p->ch, buff);
		return;
	}
	buff[i] = '0';//否则,向左走为0,向右走为1,深度也+1
	print(p->nodes[0], buff, i + 1);
	buff[i] = '1';
	print(p->nodes[1], buff, i + 1);
}

void Print(HuffmanTree* tree) {//打印所有字符的编码值
	char* buff = (char*)calloc(10, 1);//使用一个字符数组来存放当前层的前缀
	if (tree->root == NULL) {//根节点为空说明没有编码
		printf("Huffman树为空\n");
		return;
	}
	int i = 0;//用来记录层数
	print(tree->root, buff, i);
}

测试代码:

void test() {

	//输入10个小写字母以及它们的频次
	#define NUM 10
	TreeNode arr[NUM] = { 0 };//用来存放字符信息,以便生成森林
	char str[10] = { 0 };
	for (int i = 0; i < NUM; i++) {
		scanf("%s %d", str, &arr[i].times);//直接以字符串形式读入,就不必再考虑空格换行符等情况
		arr[i].ch = str[0];
	}
	HuffmanTree* p = BuildHuffmanTree(arr, NUM);

	Print(p);

	return;
}

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

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

相关文章

麻雀优化CNN超参数用于回归MATLAB

在CNN模型的构建中&#xff0c;涉及到特别多的超参数&#xff0c;比如&#xff1a;学习率、训练次数、batchsize、各个卷积层的卷积核大小与卷积核数量&#xff08;feature map数&#xff09;&#xff0c;全连接层的节点数等。直接选择的话&#xff0c;很难选到一组满意的参数&…

如何理解关系型数据库的常见设计范式?

目录理清基础概念实体属性元组分组函数依赖完全函数依赖部分函数依赖传递函数依赖码全码理解六范式第一范式&#xff08;1NF&#xff09;第二范式&#xff08;2NF&#xff09;第三范式&#xff08;3NF&#xff09;巴斯-科德范式&#xff08;BCNF&#xff0c;Boyce-Codd Normal …

基于STM32的智能家居系统设计

目录 第1章 前言 1 1.1 课题研究的背景和实际意义 1 1.1.1课题背景 1 1.1.2实际意义 1 1.2国内外发展现状、存在问题以及前景 2 1.2.1发展现状 2 1.2.2存在问题 2 1.2.3发展前景 2 1.3 主要工作、内容安排及预期成果 3 1.3.1主要研究工作 3 1.3.2预期成果 3 第2章 总体设计方案…

安卓4.X版本ssl: sslv3 alert handshake failure 握手失败

低版本https握手失败错误查看接口的协议改写代码&#xff08;网络访问采用原生的HttpsURLConnection&#xff09;参考博文&#xff1a;https://www.cnblogs.com/lwbqqyumidi/p/12063489.htmlhttps://blog.csdn.net/qq_16117167/article/details/52621112错误 如图所示&#xf…

linux进程概念

目录 1、进程的基本概念 2、进程控制块 - PCB task_struct内容分类 3、查看进程 通过ps命令查看进程 通过proc查看进程 4、通过系统调用获取进程标示符 5、通过系统调用创建进程-fork初识 6、进程状态 操作系统进程状态 linux进程状态 僵尸进程 孤儿进程 僵尸进…

计算机网络-谢希仁-第7版 第1章 概述

计算机网络-谢希仁-第7版 第1章 概述1.011.021.031.071.081.091.101.111.121.131.141.161.171.181.191.201.211.221.241.251.26计算机网络谢希仁版&#xff08;第七版&#xff09;答案 1.01 计算机网络可以向用户提供哪些服务&#xff1f; 计算机网络使用户在计算机之间传送数…

【Linux】Linux下的编译器——gcc/g++

&#x1f4ac;推荐一款模拟面试、刷题神器 、从基础到大厂面试题&#xff1a;&#x1f449;点击跳转刷题网站进行注册学习 目录 一、编译的过程 1、预处理阶段 1.1预处理的工作——头文件展开、去注释、宏替换、条件编译 1.2外部定义宏&#xff08;-D选项&#xff09; 1.…

会话跟踪技术(Cookie、Session)

目录一、CookieCookie的基本使用发送Cookie获取CookieCookie原理Cookie使用细节二、SessionSession基本使用Session原理Session使用细节三、小结四、登录注册案例会话&#xff1a;用户打开浏览器&#xff0c;访问Web服务器的资源&#xff0c;会话建立&#xff0c;直到有一方断开…

数据结构之堆的应用

系列文章目录 数据结构之堆_crazy_xieyi的博客-CSDN博客 文章目录 前言一、Top-k问题 1.前K个最小数&#xff08;第k个最小数&#xff09; 2.前K个最大数&#xff08;第K个最大数&#xff09;二、堆排序 1.从小到大排序&#xff08;建大根堆&#xff09;2.从大到…

【2022研电赛】安谋科技企业命题一等奖:基于EAIDK-610的中国象棋机器人对弈系统

本文为2022年第十七届中国研究生电子设计竞赛安谋科技企业命题一等奖作品分享&#xff0c;参加极术社区的【有奖活动】分享2022研电赛作品扩大影响力&#xff0c;更有丰富电子礼品等你来领&#xff01; 基于EAIDK-610的中国象棋机器人对弈系统 参赛单位&#xff1a;西安邮电大学…

硬件开发趋势与技术探索

LiveVideoStackCon 2022 音视频技术大会 北京站将于11月25日至26日在北京丽亭华苑酒店召开&#xff0c;本次大会将延续【音视频无限可能】的主题&#xff0c;邀请业内众多企业及专家学者&#xff0c;将他们在过去一年乃至更长时间里对音视频在更多领域和场景下应用的探索、在实…

2023届C/C++软件开发工程师校招面试常问知识点复盘Part 8

目录52、vector<string>是怎么存储的&#xff1f;53、epoll的底层原理53.1 对比select和poll53.2 ET和LT的工作模式54、进程、线程、协程的理解和他们的通信方式54.1 进程的含义54.2 线程的含义54.3 协程的含义54.4 进程间通信IPC54.5 线程间通信方式55、define宏定义的用…

【JavaDS】优先级队列(PriorityQueue),堆,Top-k问题

✨博客主页: 心荣~ ✨系列专栏:【Java实现数据结构】 ✨一句短话: 难在坚持,贵在坚持,成在坚持! 文章目录一. 堆1. 堆的概念2. 堆的存储方式3. 堆的创建4. 元素入堆5. 元素出堆6. 获取堆中元素二. 优先级堆列(PriorityQueue)1. 优先级队列2. PriorityQueue的特性3. 集合框架中P…

万字启程——零基础~前端工程师_养成之路001篇

目录 什么是前端 什么是后端 前端和后端的区别 前端工程师职责 后端工程师职责 前端的核心技术 HTML CSS JavaScript RESTful结构 特点 HTTP请求方式有哪些 目前最火的前端框架Vue vue优点 vue的响应式编程、组件化 搭建编程环境 什么是编程环境 前端的编程环…

华为云CDN,海量资源智能路由,让内容传输更快一步

华为云CDN,海量资源智能路由,让内容传输更快一步 云服务对于我们生活的影响已经愈发深入&#xff0c;在数字化转型的大背景下&#xff0c;城市管理、公共交通、医疗健康等领域都需要云服务的支持。华为云作为国内知名的云服务平台&#xff0c;以技术强、更可靠、资源多以及帮肋…

基于CentOS 7.9操作系统应用httpd配置本地镜像(本地yum源)

记录&#xff1a;301 场景&#xff1a;配置离线本地镜像源(本地yum源)的三种方式&#xff1a;直接使用iso镜像包配置、使用httpd服务应用iso镜像包配置、使用httpd服务应用rpm包配置。在内网环境或者局域网环境&#xff0c;基于CentOS 7.9操作系统应用httpd配置本地镜像(本地y…

手把手带你玩转Spark机器学习-深度学习在Spark上的应用

系列文章目录 手把手带你玩转Spark机器学习-专栏介绍手把手带你玩转Spark机器学习-问题汇总手把手带你玩转Spark机器学习-Spark的安装及使用手把手带你玩转Spark机器学习-使用Spark进行数据处理和数据转换手把手带你玩转Spark机器学习-使用Spark构建分类模型手把手带你玩转Spa…

Python学习笔记(十三)——编译错误和异常处理

异常和异常类 Python常见错误 语法错误 源代码存在拼写语法错 误&#xff0c;这些错误导致Python 编译器无法把Python源代 码转换为字节码&#xff0c;故也称 之为编译错误。>>> print("我爱山大"} SyntaxError: invalid syntax 运行时错误 • 程序中没有…

Python常用库1:collections,容器数据类型

collections&#xff1a;数据容器 点这里跳到原文地址。预计阅读时长&#xff1a;10分钟未完待续&#xff0c;遇到相关力扣题目&#xff0c;会继续补充~ 文章目录前言一、Collections中的内置函数二、各个函数的使用1. deque1.1 deque的介绍1.2 deque支持的方法1.3 使用deque解…

js-键盘事件

onkeydown:按键被按下 onkeyup:按键被松开 事件绑定的对象&#xff1a;键盘事件一般绑定给可以获取焦点的对象或者document对象 焦点&#xff1a;光标在闪的&#xff1a;比如input标签 如果一直按按键不松手&#xff0c;按键会一直被触发 当&#xff1a;onkeydown连续触发时…