【贪心算法·哈夫曼编码问题】从定长编码和不定长编码讲到最小化带权路径长度和

一、问题介绍

1.1:编码问题

首先,我们知道,数字字符等任何数据的底层,都是以二进制(0,1序列)的方式存储在计算机内的。

对于“编码”其实就是那些能显示在计算机屏幕上的:不同字母、汉字、字符所分别对应的二进制序列的一种映射关系。

通常的编码方式有固定长度编码不定长度编码两种。

固定长度编码:就是每一个字符对应的二进制01序列的长度是相同的,不如Unicode、UTF-8、GB2312、GBK,ACSII都是定长编码
在这里插入图片描述
优点:每个字符对应的字符序列长度相同,易读取(不会产生二义性问题,依次往后按照固定长度读取即可得到字符)

缺点:计算机的传输资源(带宽)是有限的,如果每个字符都是等长的,当有的字符频率高,有的字符频率低,有的字符可能根本没出现,其实再使用等长编码就会浪费资源。

举例:

字母二进制
A000
B001
C010
D011
E100
F101
G110
H111

在这种编码下,如果文件内容是“ABCDEFGH"或者几个字符出现次数都比较均匀的情况下,这种编码方式其实也行,但是很多情况,有的字符频率会特别高,有的字符会特别低,如果整个都采用相同长度的编码,会浪费存储和传输资源。如果 让那些频率高的字符对应存储的二进制序列长度短,而频率低的可以适当长一些, 这样就可以大量减少资源的浪费了。

于是产生了不定长编码

不定长度编码:它的设计思想就是始得总体的编码长度之和尽可能小。不定长编码需要解决以下两个关键问题

  1. “编码不可以有二义性”: 即一个字符的编码不可以是另外一个字符的前缀码,否则无法判断这是一个字符还是一个字符的一部分,比如:“01”和“010”就不能同时作为两个不同字符的编码。

  2. ”编码长度尽可能短": 让出现频率高的字符的编码长度短,而频率低的字符编码可以稍长;从而压缩总共的存储空间,提高传输的速度。

优点:很明显,存储空间压缩,传输性能提升。

缺点:设计时需考虑避免二义性问题。

不定长编码有多种,而如何设计,能让一段文字对应的编码长度总和最短呢?—— 哈夫曼编码:一种【贪心思想】的不定长编码策略,能使总编码长度最短。

二、哈夫曼编码

2.1:目标:最小化带权路径长度和WPL

不定长编码首要解决的问题是: “编码不可以有二义性"(即应该都为前缀码)。我们现在先把这个前提记在脑子里。

前缀码(prefix code)是在计算机科学中用于表示数据或信息的编码方式。在前缀码中有一个关键要求是任一编码都不是其他编码的前缀。这个特性使得解码器在任何时候都能够唯一确定地识别编码前缀并进行解码。

而编码过程通常就是不断在0,1之间在做选择的过程,所以在解决编码问题时,通常都会使用二叉树这种数据结构。

在这里插入图片描述
如上图所示,是使用二叉树进行给字符编码的示例,自顶而下每条边代表01,遍历到那个节点,这条边上的0,1序列即为其字符的编码。但是考虑到上文说到的前缀码问题,我们编码的字符必须只能是这颗树上的叶子节点(你可以看到,如果非叶子节点,那么必然这个字符的码是其他字符码的前缀!!!)。

当我们为需要编码的字符构建这样一颗使叶子节点是需要编码的字符的二叉树,我们就得到了各个字符的边长编码。

  • 编码为从根节点出发,到该字符叶子节点路径上的边的0,1序列
  • 长度为根节点到该字符叶子节点的长度。

等等,那么我们该如何构建呢?或者说,如何构建一颗最优的非定长编码二叉树?

我不想直接说哈夫曼编码,我想一步一步,能不能我们自己推出来哈夫曼编码这个算法!站在前人肩膀上固然是好的,但是有的时候思维总是受限,殊不知前人得出这个绝妙想法也是有一个思想的轨迹的,或许是受到知识的诅咒,总是拆掉获取真理的那个阶梯而直接向世人抛出结果,但我认为,真正珍贵的是,是他思考的轨迹!!!~~扯远了,这里从我自己思考哈夫曼编码的思考轨迹出发,来试着摸索着哈夫曼编码是如何被提出的…

OK,我们构建定长编码的本意就是为了压缩存储空间,我们的目的就是: 在当前字符和其频率确定的情况下,为其设计一种编码方式,最小化其存储空间!

既然我们有了对字符编码的这棵二叉树,那么我们在这个二叉树上,可以以二叉树相关问题的形式把我们的这个目标描述一下,这也是我们之后基于这么一颗编码二叉树求解的核心。

在这之前,我们需要对以下概念有一些了解:

概念定义
路径从树中一个结点到另一个结点之间的分支构成这两个结点之间的路径
路径长度路径上的分支数目
树的路径长度从树根到每一结点的路径长度之和
赋予某个实体的一个量,是对实体的某个或某些属性的数值化描述。在数据结构中,实体有结点(元素)和边(关系)两大类,所以对应有结点权和边权。结点权或边权具体代表什么意义,需要具体问题具体分析。如果在一棵树中的结点上带有权值,则对应的就有带权树等概念。
结点带权路径长度从该结点到树根之间的路径长度与结点上权的乘积
树的带权路径长度(WPL)树中所有叶子结点的带权路径长度之和,通常记作 WPL

还是以下面两棵编码树举例,其实如果以1bit为单位,我们所需要的存储空间就是 树的带权路径长度(WPL)
在这里插入图片描述
左树 WPL:1 × 2 + 3 × 2 + 2 × 2 + 4 × 3 + 5 × 3 = 39
右树 WPL:1 × 1 + 2 × 2 + 3 × 3 + 4 × 4 + 5 × 4 = 50

所以我们利用编码二叉树求解最优非定长编码的目标就是:最小化带权路径长度(Weighted Path Length,WPL),即所有字符的频率乘以其在树中的路径长度的总和。

2.2:如何最小化WPLor如何构建最优编码二叉树:贪心

还是上面两棵二叉树的图,那么如何构建这样一颗二叉树呢?我们现在知道是叶子节点(也就是待编码的字符), 如何利用这些带权叶子节点构建一颗WPL最小的编码二叉树?我们想要WPL尽量小,我们就要让频率小的叶子节点尽量都往树的底下靠(这样路径长,但由于频率小,对总WPL的影响小),频率较高的叶子节点尽量靠近根节点, 这样还不够,还需要使总共的WPL也最小。

1、方法1:自顶向下❌

构建树,我们往往会很自然而然的想到自顶向下,本着让频率小的叶子节点尽量都往树的底下靠(这样路径长,但由于频率小,对总WPL的影响小),频率较高的叶子节点尽量靠近根节点的目的,我们 自顶向下,每次选择频率最高的叶子节点作为根节点的孩子,如果还有频率次高,则再作为子孩子,这样可以吗?

在这里插入图片描述

但是你发现,虽然我们把频率由高到低,依次从根节点往下放,听起来很合理,但其实得到的结果并不是全局最优!!!因为我们做选择就不是局部最优,比如我们把D放在根节点的左孩子位置,其实这不是一个局部最优的做法,因为这就意味着这个分支不能往下存在叶子节点。比如我很容易就能找出下面这个反例,它的WPL就比上面那棵树的低:
在这里插入图片描述

既然自顶向下构建不行,那么自底向上呢??

🪧毕竟我们都知道这棵树的叶子节点了,只要我们把这些叶子节点进行合并,依次由底向上,也可以构建一颗编码树,但是注意,我们还是要思考如何能获得一个最优(WPL最小)的编码二叉树!!

2. 方法2:自底向上✅

首先先不管WPL怎么最小,我们看看如何由叶子节点自底向上构建二叉树吧。
在这里插入图片描述
上图只是自底向上由叶子节点构建二叉树的其中一种方式,我们需要get到的核心是构建的步骤:

  1. 取两个叶子节点合并成一个新节点
  2. 将新节点加入到叶子节点集合
  3. 重复1,2直到叶子节点集合为只剩一个节点

OK,明白了如何由叶子节点构建二叉树,我们现在需要思考的就是我们的目标本身了:如何使这个二叉树的WPL(带权路径和)最小?

同样的,本着让频率小的叶子节点尽量都往树的底下靠(这样路径长,但由于频率小,对总WPL的影响小),频率较高的叶子节点尽量靠近根节点的目的:

  1. 首先选频率最小的两个节点合并,得到新节点(其权值为两个频率最小节点的权值和)——局部最优
  2. 将得到的新节点加入叶子节点集合
  3. 重复1,2
  4. 直到叶子节点集合只剩一个节点——全局最优

在这里插入图片描述

上面这个步骤明显是一个贪心过程:我们贪的就是在由叶子节点构建二叉树时,总是选择节点集合中频率最小的两个进行合并!

为什么这个贪心的局部最优成立?关键在于:将叶子节点进行合并成新节点后,得到的新节点频率是从这个集合中任意选出两个节点合并最低的,这让他在后续构建过程中可以被优先合并(处于这棵树的较低层);同时,不像自上而下,构建出的新节点并不会限制后续的构建过程(不会影响全局最优)。

说实话贪心确实挺难证明的,一般情况下,你只要举不出反例,其实就是OK的。就比如说最典型的一个贪心,有一堆钱,你只能拿十张,你是不是每次都是拿里面最大的,拿完十张就是整体最大;那我要你证明,其实你说该怎么证明呢?也不好证明。

像下面这张图展示的,如果每次不是从节点集合中选择频率最小的,其实最后得出来的结果就是没有上面那种大
在这里插入图片描述

2.3:自顶向下的贪心思想

虽然上面讨论出来,在构建二叉编码树这个过程,的确是自底向上的,但是它所用到的贪心思想确实是:自顶向下。为什么这么说呢?

原问题有n个节点,通过贪心选择合并两个频率最低节点为一个新节点,将新节点加入到节点集合,进行n-1个节点的最优编码二叉树构建,可以看到,首先我们要承认使用贪心的前提是:问题具有贪心选择性质,即可以通过局部最优进而得到全部最优(并不是所有问题都具有贪心选择性质);而由局部最优得到全局最优其实是一个自顶向下的思想过程:通过每次当前的贪心选择,来缩小问题规模,从而一步一步得到全局最优解。

而动态规划则和分治是典型的自底向上的过程,通过小规模问题,向上推导or合并得到最终问题;而贪心则是一开始就站在了一个全局的视角(它必须这样看,否则不能判断当前局部最优是否能推导整体最优),通过贪心选择,来缩小问题规模。

所以我想说的是,自顶向下的是贪心的思想框架,而哈夫曼树构建的实际过程,的确是自底向上。

2.4:实现代码:

#include <iostream>
#include <unordered_map>
#include <queue>using namespace std;//1.定义哈夫曼树的节点
struct Node{char ch; //字符int freq; //频率Node* left;Node* right;//c++语法:构造函数Node(char character, int frequency): ch(character), freq(frequency), left(nullptr), right(nullptr){}
};//2.比较函数,用于优先队列构建小根堆
struct Compare{bool operator()(Node* node1, Node* node2){return node1->freq > node2->freq;}
};//3.哈夫曼树自底向上构建(传入字符+频率集合
Node* buildHuffmanTree(const unordered_map<char, int>& frequencies){//小根堆(优先队列实现),用来存储实际节点Node,且按频率大小排序priority_queue<Node*, vector<Node*>, Compare> minHeap;//创建叶子节点并且加入优先队列for(const auto& pair : frequencies){minHeap.push(new Node(pair.first, pair.second));}//自底向上合并节点并插入优先队列while(minHeap.size()>1){//Step1:取出频率最小的两个节点Node* leftNode = minHeap.top();minHeap.pop();Node* rightNode = minHeap.top();minHeap.pop();//Step2:合并Node* mergedNode = new Node('\0', leftNode->freq + rightNode->freq);mergedNode->left = leftNode;mergedNode->right = rightNode;//Step3:将新节点加入优先队列minHeap.push(mergedNode);}//返回根节点return minHeap.top();}//根据哈夫曼树获取各个字符的哈夫曼编码——二叉树的遍历,prefix记录当前路径上的哈夫曼编码void getCodes(Node* root, const string& prefix, unordered_map<char, string>& huffmanCodes){if(!root) return;//遍历边,寻找叶子节点if(!root->left && !root->right){huffmanCodes[root->ch] = prefix;}getCodes(root->left, prefix+"0", huffmanCodes);getCodes(root->right, prefix+"1", huffmanCodes);
}//释放哈夫曼树内存// 释放哈夫曼树内存
void freeHuffmanTree(Node* root) {if (!root) return;freeHuffmanTree(root->left);freeHuffmanTree(root->right);delete root;
}int main(){// 输入字符和频率unordered_map<char, int> frequencies;int n;cout << "Enter the number of characters: ";cin >> n;for (int i = 0; i < n; ++i) {char ch;int freq;cout << "Enter character and frequency: ";cin >> ch >> freq;frequencies[ch] = freq;}// 构建哈夫曼树Node* root = buildHuffmanTree(frequencies);// 生成哈夫曼编码unordered_map<char, string> huffmanCodes;getCodes(root, "", huffmanCodes);// 输出每个字符的哈夫曼编码cout << "Huffman Codes:\n";for (const auto& pair : huffmanCodes) {cout << pair.first << ": " << pair.second << endl;}// 释放哈夫曼树内存freeHuffmanTree(root);return 0;
}

在这里插入图片描述

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

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

相关文章

半导体光子电学期末笔记2: 光子晶体 Photonic crystals

光子晶体概述 光子晶体定义和分类 [P4-5] 光子晶体是一种在一维、二维或三维空间内周期性排列的多层介质。这些结构通过在光子尺度上排列的重复单元&#xff0c;可以对光进行调控和控制。具体来说&#xff0c;光子晶体是指那些在空间上具有周期性排列的介质结构&#xff0c;它…

【深度学习】温故而知新4-手写体识别-多层感知机+CNN网络-完整代码-可运行

多层感知机版本 import torch import torch.nn as nn import numpy as np import torch.utils from torch.utils.data import DataLoader, Dataset import torchvision from torchvision import transforms import matplotlib.pyplot as plt import matplotlib import os # 前…

wvp-gb28181-pro搭建流媒体服务器,内存占用过高问题

直接给出解决办法&#xff0c;端口暴露的太多了&#xff0c;暴露了500个端口导致从3g---->11g 遇到的问题&#xff0c;直接使用镜像《648540858/wvp_pro:latest》在宿主机上运行,如我下面的博客 https://blog.csdn.net/weixin_41012767/article/details/137112338?spm100…

FASTGPT:可视化开发、运营和使用的AI原生应用

近年来&#xff0c;随着人工智能&#xff08;AI&#xff09;技术的迅猛发展&#xff0c;AI的应用逐渐渗透到各行各业。作为一种全新的开发模式&#xff0c;AI原生应用正逐步成为行业的焦点。在这方面&#xff0c;FASTGPT无疑是一款颇具代表性的产品。本文将详细介绍FASTGPT的设…

Appium自动化环境搭建保姆级教程

APP自动化测试运行环境比较复杂&#xff0c;稍微不注意安装就会失败。我见过不少朋友&#xff0c;装了1个星期&#xff0c;Appium 的运行环境还没有搭好的。 搭建环境本身不是一个有难度的工作&#xff0c;但是 Appium 安装过程中确实存在不少隐藏的比较深的坑&#xff0c;如果…

用友NC pagesServlet SQL注入致RCE漏洞复现(XVE-2024-13067)

0x01 产品简介 用友NC是由用友公司开发的一套面向大型企业和集团型企业的管理软件产品系列。这一系列产品基于全球最新的互联网技术、云计算技术和移动应用技术,旨在帮助企业创新管理模式、引领商业变革。 0x02 漏洞概述 用友NC /portal/pt/servlet/pagesServlet/doPost接口…

操作系统入门系列-MIT6.828(操作系统工程)学习笔记(三)---- xv6初探与实验一(Lab: Xv6 and Unix utilities)

系列文章目录 操作系统入门系列-MIT6.S081&#xff08;操作系统&#xff09;学习笔记&#xff08;一&#xff09;---- 操作系统介绍与接口示例 操作系统入门系列-MIT6.828&#xff08;操作系统工程&#xff09;学习笔记&#xff08;二&#xff09;----课程实验环境搭建&#x…

RocketMQ相关知识知多少

一、RocketMQ的定义 官网网址&#xff1a;领域模型概述 | RocketMQ Apache RocketMQ 自诞生以来&#xff0c;因其架构简单、业务功能丰富、具备极强可扩展性等特点被众多企业开发者以及云厂商广泛采用。历经十余年的大规模场景打磨&#xff0c;RocketMQ 已经成为业内共识的金…

Flink的简单学习二

一 Flink的核心组件 1.1 client 1.将数据流程图DataFlow发送给JobManager。 1.2 JobManager 1.收集client的DataFlow图&#xff0c;将图分解成一个个的task任务&#xff0c;并返回状态更新数据给client 2.JobManager负责作业调度&#xff0c;收集TaskManager的Heartbeat和…

三、基于图像分类预训练编码及图神经网络的预测模型 【框图+源码】

背景&#xff1a; 抽时间补充&#xff0c;先挖个坑。 一、模型结构 二、源码

美团一面:什么是CAS?有什么优缺点?我说你说的是AtomicInteger吗?

引言 传统的并发控制手段&#xff0c;如使用synchronized关键字或者ReentrantLock等互斥锁机制&#xff0c;虽然能够有效防止资源的竞争冲突&#xff0c;但也可能带来额外的性能开销&#xff0c;如上下文切换、锁竞争导致的线程阻塞等。而此时就出现了一种乐观锁的策略&#x…

企业数字化转型的测度难题:基于大语言模型的新方法与新发现

《经济研究》新文章《企业数字化转型的测度难题&#xff1a;基于大语言模型的新方法与新发现》运用机器学习和大语言模型构造一套新的企业数字化转型指标。理论分析和数据交叉验证均表明&#xff0c;构建的指标相对已有方法更准确&#xff1a; 1.第一步&#xff1a;选择“管理…

17.Redis之主从复制

1.主从复制是怎么回事&#xff1f; 分布式系统, 涉及到一个非常关键的问题: 单点问题 单点问题&#xff1a;如果某个服务器程序, 只有一个节点(只搞一个物理服务器, 来部署这个服务器程序) 1.可用性问题,如果这个机器挂了,意味着服务就中断了~ 2.性能/支持的并发量也是比较有限…

【HarmonyOS】鸿蒙系统中应用权限等级介绍、定义、申请授权讲解

【HarmonyOS】鸿蒙系统中应用权限等级介绍、定义、申请授权讲解 针对权限等级&#xff0c;相对于主体来说&#xff0c;会有不同的细分概念。 一、权限APL等级&#xff1a; 首先在鸿蒙系统中&#xff0c;对于权限本身&#xff0c;分为三个等级&#xff1a;normal&#xff0c;s…

SQL面试问题集

目录 Q.左连接和右连接的区别 Q.union 和 union all的区别 1、取结果的交集 2、获取结果后的操作 Q.熟悉开窗函数吗&#xff1f;讲一下row_number和dense_rank的区别。 Q.hive行转列怎么操作的 Q.要求手写的题主要考了聚合函数和窗口函数&#xff0c;row_number()&#…

同一个tomcat不同端口运行不同项目

第一步&#xff1a;修改 server.xml 文件 修改 tomcat 安装目录下 conf/server.xml 文件&#xff0c;需要几个端口就添加几个 Service 节点。 配置 2 个端口&#xff1a;9131 和 9133&#xff0c;于是增加两个 Service 节点。 每个 Service 节点的 name 属性值要设置不同的值…

【MATLAB】雷达信号处理程序源码 雷达系统仿真代码 matlab SAR

【MATLAB】雷达信号处理程序源码 雷达系统仿真代码 matlab SAR 包含以下所有源码,内容如下&#xff1a;&#xff1a; 1、 MATGPR R3探地雷达数据处理 MATLAB 程序 2、 python 雷达图像识别 3、 SAR 雷达回波仿真 matlab 4、 SAR 雷达影像处理源码 5、 STFT 处理 IPIX 雷达…

数据分析常用模型合集(三)同期群、逻辑树、假设检验等

前面两篇文章&#xff0c;我们将比较大、较为系统的分析方法作了一个介绍&#xff0c;本文是最后一篇&#xff0c;将剩余的一些讲一讲。 数据分析常用模型合集&#xff08;二&#xff09;RARRA模型、RFM模型-CSDN博客 剩下的一些模型&#xff0c;其实不应叫做模型&#xff0c;…

qt+ffmpeg 实现音视频播放(四)之音视频同步

在处理音视频数据时&#xff0c;解码音频的数据往往会比解码视频的数据比较慢&#xff0c;所以我们在播放音视频时&#xff0c;音频和视频的数据会出现渐渐对不上的情况。尤其在播放时间越长的时候&#xff0c;这种对不上的现象越明显。 为了解决这一问题&#xff0c;人们想出…

在windows操作系统上安装MariaDB

最近收到关于数据库在哪里看的评论&#xff0c;所以就一不做二不休&#xff0c;把安装数据库的步骤写一篇文章吧。 这篇文章介绍如何在windows上完成MariaDB-10.6.5版本的安装&#xff0c;对应MySQL-8.x版本。 第一步&#xff1a;下载安装包 通过以下网盘链接下载MariaDB-10.6…