从零开始的数据结构教程(六) 贪心算法
🍬 标题一:贪心核心思想——发糖果时的最优分配策略
贪心算法 (Greedy Algorithm) 是一种简单直观的算法策略。它在每一步选择中都采取在当前状态下最好或最优(即最有利)的选择,从而希望得到一个全局最优解。这就像你作为班主任发糖果,每次都想让眼前的孩子满意,希望这种局部最优能带来整体最优的效果。
三大适用条件
并非所有问题都适合用贪心算法解决,它通常需要满足以下三个条件:
- 贪心选择性质 (Greedy Choice Property):每一步的局部最优选择,都能导致最终的全局最优解。例如,在“硬币找零”问题中,如果硬币面额系统是“标准”的(如 1, 5, 10, 25 美分),那么每次都优先选择最大面额的硬币就能得到最少硬币数。
- 无后效性 (Optimal Substructure):当前的选择不会影响后续子问题的最优解,即后续问题的最优解不依赖于之前的选择,只依赖于当前状态。例如,在“区间调度”中,一旦你选择了最早结束的会议,后续会议的选择空间并不会因此受限,而是基于剩余时间继续做决策。
- 子问题可合并 (Overlapping Subproblems):虽然这是动态规划的特征之一,但在贪心算法中,这意味着局部解能够直接或以简单的方式构成全局解。例如,在霍夫曼编码中,每次合并两个最小频率的节点,最终能构建出最优的二叉树。
与 DP 的关键区别
贪心算法和动态规划都涉及将问题分解为子问题,但它们在解决方式上存在根本差异:
- 贪心:做选择时不考虑未来,只看当前局部最优,并且一旦做出选择就不可回退。它不需要存储所有子问题的解。
- 动态规划:会探索所有可能的局部最优解,并存储它们,通过状态转移方程来构建全局最优解。它具有“回退”机制,可以从之前的多种选择中推导出当前的最优。
# 贪心 vs 动态规划:以「硬币找零」为例
# 假设硬币面额为 [1, 5, 10, 25],找零 30
# 贪心:25 + 5 (2枚)
# DP:10 + 10 + 10 (3枚) 或 25 + 1 + 1 + 1 + 1 + 1 (6枚)# 贪心(可能无法得到最优解,例如面额 [1, 15, 25], 找零 30, 贪心会是 25+1+1+1+1+1 (6枚), 最优是 15+15 (2枚))
def coinChangeGreedy(coins, amount):coins.sort(reverse=True) # 优先使用大面额硬币count = 0for coin in coins:while amount >= coin:amount -= coincount += 1return count if amount == 0 else -1 # 如果 amount 不为 0,说明无法找零# DP(保证最优解)
def coinChangeDP(coins, amount):# dp[i] 表示凑齐金额 i 所需的最少硬币数dp = [float('inf')] * (amount + 1)dp[0] = 0 # 凑齐金额 0 需要 0 枚硬币for i in range(1, amount + 1): # 遍历所有可能的金额for coin in coins: # 遍历所有硬币面额if i >= coin:# 如果当前金额 i 大于等于当前硬币面额 coin# 那么凑齐 i 的最少硬币数可能是:# 1. 不使用当前硬币,沿用 dp[i] (之前可能计算过)# 2. 使用当前硬币,那么剩下的金额 i - coin 需要 dp[i - coin] 枚硬币dp[i] = min(dp[i], dp[i - coin] + 1)return dp[amount] if dp[amount] != float('inf') else -1 # 如果为 inf,表示无法凑齐# print(coinChangeGreedy([1, 15, 25], 30)) # 输出 6
# print(coinChangeDP([1, 15, 25], 30)) # 输出 2
⏰ 标题二:区间调度问题——会议室的高效安排
区间调度问题是一个经典的贪心算法应用场景。
经典问题
给定若干会议的开始和结束时间 [start, end]
,如何安排最多的不重叠会议,使得一个会议室可以安排尽可能多的会议?
贪心策略
按结束时间排序:优先选择最早结束的会议。为什么?因为最早结束的会议会给后续会议留出最大的时间空隙,从而使得我们有机会安排更多的会议。
import java.util.Arrays;
import java.util.Comparator; // 引入 Comparator 接口class Solution {public int maxEvents(int[][] intervals) {// Step 1: 按结束时间升序排序会议// 如果结束时间相同,则按开始时间升序排序(可选,但通常不影响结果)Arrays.sort(intervals, (a, b) -> a[1] - b[1]);int count = 0; // 记录安排的会议数量int lastEndTime = Integer.MIN_VALUE; // 记录上一个已安排会议的结束时间// Step 2: 遍历排序后的会议for (int[] meeting : intervals) {int currentStart = meeting[0];int currentEnd = meeting[1];// 如果当前会议的开始时间 >= 上一个已安排会议的结束时间// 说明当前会议可以安排,且不与之前的会议重叠if (currentStart >= lastEndTime) {count++; // 安排该会议lastEndTime = currentEnd; // 更新上一个会议的结束时间}}return count;}
}
变形题
- 需要多间会议室时:这通常会转化为其他问题,例如“最小箭射气球”(LeetCode 452),它寻找最少数量的“箭头”能射爆所有气球(代表区间),本质上是寻找最少数量的区间来覆盖所有给定区间。
- 带权值的区间:如果每个会议有不同的“价值”或“收益”,需要最大化总收益,那么纯粹的贪心可能不再适用,通常需要结合动态规划来解决(例如“最大收益的兼职工作”)。
🏃 标题三:跳跃游戏——从起点到终点的最少步数
跳跃游戏系列问题也是贪心算法的经典应用。
两类经典问题
-
能否到达终点(LeetCode 55):给定一个非负整数数组
nums
,每个元素nums[i]
代表你在位置i
最多可以向前跳跃的长度。判断你是否能从第一个位置跳到最后一个位置。def canJump(nums):max_reach = 0 # 记录当前能到达的最远位置# 遍历数组,直到到达终点或无法继续前进for i in range(len(nums)):if i > max_reach: # 如果当前位置 i 已经超出了之前能到达的最远位置return False # 说明无法到达终点# 更新能到达的最远位置:当前位置 i + 从当前位置能跳的距离 nums[i]max_reach = max(max_reach, i + nums[i])if max_reach >= len(nums) - 1: # 如果能到达或越过终点return True # 则成功到达return True # 如果循环结束(即已经遍历完数组),说明可以到达终点
-
最少跳跃次数(LeetCode 45):给定一个非负整数数组
nums
,同上,求到达最后一个位置的最少跳跃次数。def jump(nums):if len(nums) <= 1:return 0jumps = 0 # 跳跃次数current_end = 0 # 当前跳跃能到达的最远边界farthest = 0 # 在当前跳跃范围内,下一步能到达的最远位置for i in range(len(nums) - 1): # 遍历到倒数第二个位置,因为最后一个位置不需要再跳# 更新在当前跳跃范围内能到达的最远位置farthest = max(farthest, i + nums[i])if i == current_end: # 如果当前位置 i 达到了当前跳跃的边界jumps += 1 # 进行一次跳跃current_end = farthest # 更新下一次跳跃的边界为当前能到达的最远位置# 注意:这里不需要检查 farthest 是否能到达终点,# 因为 for 循环条件保证了最终 current_end 会达到或超过 len(nums) - 1return jumps
关键洞察
贪心地在每一步选择能跳最远的选项(但不是盲目地跳到最远,而是规划好下一次跳跃的边界)。
🛒 标题四:高频面试题——分发糖果(双向贪心)
问题
在一个班级里,有 n
个孩子,他们的能力值用整数数组 ratings
表示。你需要给这些孩子分发糖果,并满足以下两个条件:
- 每个孩子至少分到 1 颗糖果。
- 能力值比其相邻孩子高的孩子,必须分到比相邻孩子更多的糖果。
求最少需要分发多少颗糖果。
双向遍历策略
这个问题不能简单地从左到右或从右到左遍历一次,因为它有双向的依赖关系。需要进行双向贪心:
- 从左到右遍历:确保右边的孩子比左边高的,能获得更多糖果。
- 从右到左遍历:确保左边的孩子比右边高的,能获得更多糖果。
- 取两者最大值:最终每个孩子分到的糖果数,是两次遍历结果的最大值。
def candy(ratings):n = len(ratings)# left 数组:从左到右遍历,保证 ratings[i] > ratings[i-1] 时,left[i] > left[i-1]left = [1] * n # 初始化每个孩子至少1颗糖for i in range(1, n):if ratings[i] > ratings[i - 1]:left[i] = left[i - 1] + 1# right 变量:从右到左遍历,保证 ratings[i] > ratings[i+1] 时,当前孩子获得更多糖# total:总糖果数,先加上 left 数组的最后一个元素right = 1total = left[n - 1] # 最后一个孩子的糖果数,至少是 left[n-1]for i in range(n - 2, -1, -1): # 从倒数第二个孩子开始,向左遍历if ratings[i] > ratings[i + 1]:right += 1 # 如果当前孩子比右边高,right 增加else:right = 1 # 否则,重置 right 为 1 (因为右边孩子可能比自己高或相等)# 当前孩子实际获得的糖果数,是 left[i] 和 right 两者中的最大值# 这样才能同时满足左右两个方向的条件total += max(left[i], right)return total
- 时间复杂度: O ( n ) O(n) O(n),因为进行了两次线性遍历。
- 空间复杂度: O ( n ) O(n) O(n),因为使用了
left
数组。可以进一步优化到 O ( 1 ) O(1) O(1) 空间,但代码会更复杂。
📊 总结表:贪心算法适用场景
问题类型 | 典型例题 | 贪心策略 |
---|---|---|
区间问题 | 无重叠区间(LeetCode 435) | 按结束时间排序,优先选择最早结束的。 |
分配问题 | 分发糖果(LeetCode 135) | 双向遍历(从左到右,从右到左)满足约束。 |
跳跃问题 | 跳跃游戏 II(LeetCode 45) | 维护当前能到达的最远位置,并在必要时进行跳跃。 |
字符串构造 | 重构字符串(LeetCode 767) | 优先使用剩余最多的字符,避免连续。 |
硬币/货币 | 硬币找零(标准面额系统) | 优先使用最大面额的硬币。 |
相关文章:
从零开始的数据结构教程(六) 贪心算法
🍬 标题一:贪心核心思想——发糖果时的最优分配策略 贪心算法 (Greedy Algorithm) 是一种简单直观的算法策略。它在每一步选择中都采取在当前状态下最好或最优(即最有利)的选择,从而希望得到一个全局最优解。这就像你…...

Spring框架学习day7--SpringWeb学习(概念与搭建配置)
SpringWeb1.SpringWeb特点2.SpringWeb运行流程3.SpringWeb组件4.搭建项目结构图:4.1导入jar包4.2在Web.xml配置**4.2.1配置统一拦截分发器 DispatcherServlet**4.2.2开启SpringWeb注解(spring.xml) 5.处理类的搭建6.SpringWeb请求流程(自己理…...

打造高效多模态RAG系统:原理与评测方法详解
引言 随着信息检索与生成式AI的深度融合,检索增强生成(RAG, Retrieval-Augmented Generation) 已成为AI领域的重要技术方向。传统RAG系统主要依赖文本数据,但真实世界中的信息往往包含图像、表格等多模态内容。多模态RAG…...
SSM 框架核心知识详解(Spring + SpringMVC + MyBatis)
🌱 第一部分:Spring 核心原理与使用 1. 什么是 Spring Spring 是一个开源的 Java 企业级开发框架,旨在简化 Java 企业应用程序开发。它核心思想是控制反转(IoC)和面向切面编程(AOP)࿰…...
1.2 fetch详解
浏览器 Fetch API 详解 Fetch API 是现代浏览器提供的用于发起网络请求的接口,它基于 Promise 实现,替代了传统的 XMLHttpRequest,提供了更强大、更灵活的功能。 1. 基本用法 使用 fetch() 函数发起请求,返回一个 Promise&…...

【C#】Quartz.NET怎么动态调用方法,并且根据指定时间周期执行,动态配置类何方法以及Cron表达式,有请DeepSeek
🌹欢迎来到《小5讲堂》🌹 🌹这是《C#》系列文章,每篇文章将以博主理解的角度展开讲解。🌹 🌹温馨提示:博主能力有限,理解水平有限,若有不对之处望指正!&#…...

02 Deep learning神经网络的编程基础 逻辑回归--吴恩达
逻辑回归 逻辑回归是一种用于解决二分类任务(如预测是否是猫咪等)的统计学习方法。尽管名称中包含“回归”,但其本质是通过线性回归的变体输出概率值,并使用Sigmoid函数将线性结果映射到[0,1]区间。 以猫咪预测为例 假设单个样…...
Android Native 内存泄漏检测全解析:从原理到工具的深度实践
引言 Android应用的内存泄漏不仅发生在Java/Kotlin层,Native(C/C)层的泄漏同样普遍且隐蔽。由于Native内存不受Java虚拟机(JVM)管理,泄漏的内存无法通过GC自动回收,长期积累会导致应用内存占用…...
React---扩展补充
一些额外的扩展 4.3 高阶组件 高阶组件是参数为组件,返回值为新组件的函数; 高阶组件 本身不是一个组件,而是一个函数;其次,这个函数的参数是一个组件,返回值也是一个组件; import React fr…...
HTML 中 class 属性介绍、用法
1、🔖 什么是 class class 是 HTML 元素的一个核心属性,用来为元素指定一个或多个类名。它在网页开发中承担三大作用: 🎨 连接样式(CSS):让元素应用预定义的视觉效果⚙️ 绑定行为(…...

MySQL的并发事务问题及事务隔离级别
一、并发事务问题 1). 赃读:一个事务读到另外一个事务还没有提交的数据。 比如 B 读取到了 A 未提交的数据。 2). 不可重复读:一个事务先后读取同一条记录,但两次读取的数据不同,称之为不可重复读。 事务 A 两次读取同一条记录&…...

ProfiNet 分布式 IO 在某污水处理厂的应用
随着城市化进程的加速,污水处理厂的规模和复杂性不断增加,对自动化控制系统的要求也越来越高。PROfinet 分布式 IO 作为一种先进的工业通信技术,以其高速、可靠、灵活的特性,为污水处理厂的自动化升级提供了有力支持。本文将结合某…...
vue2使用笔记、vue2和vue3的区别
文章目录 vue2和vue3的区别1. 实现数据响应式的原理不同2. 生命周期不同3. vue 2.0 采用了 option 选项式 API,vue 3.0 采用了 composition 组合式 API4. 新特性编译宏5. 父子组件间双向数据绑定 v-model 不同6. v-for 和 v-if 优先级不同7. 使用的 diff 算法不同8.…...
Vue2数组数字字段求和技巧 数字求和方法
<template><div><p>总和: {{ totalSum }}</p></div> </template><script> export default {data() {return {items: [{ id: 1, value: 10 },{ id: 2, value: 20 },{ id: 3, value: 30 }]};},computed: {totalSum() {return this.ite…...

vue2 , el-select 多选树结构,可重名
人家antd都支持,elementplus 也支持,vue2的没有,很烦。 网上其实可以搜到各种的,不过大部分不支持重名,在删除的时候可能会删错,比如树结构1F的1楼啊,2F的1楼啊这种同时勾选的情况。。 可以全…...

Excel处理控件Aspose.Cells教程:使用 C# 从 Excel 进行邮件合并
邮件合并功能让您能够轻松批量创建个性化文档,例如信函、电子邮件、发票或证书。您可以从模板入手,并使用电子表格中的数据进行填充。Excel 文件中的每一行都会生成一个新文档,并在正确的位置包含正确的详细信息。这是一种自动化重复性任务&a…...
Jenkins | Jenkins构建成功服务进程关闭问题
Jenkins构建成功服务进程关闭问题 1. 原因2. 解决 1. 原因 Jenkins 默认会在构建结束时终止所有由构建任务启动的子进程,即使使用了nohup或后台运行符号&。 2. 解决 在启动脚本中加上 BULID_IDdontkillme #--------------解决jenkins 自动关闭进程问题-----…...
模块化架构下的前端调试体系建设:WebDebugX 与多工具协同的工程实践
随着前端工程化的发展,越来越多的项目采用模块化架构:单页面应用(SPA)、微前端、组件化框架等。这类架构带来了良好的可维护性和复用性,但也带来了新的调试挑战。 本文结合我们在多个模块化项目中的真实经验ÿ…...

EXCEL通过DAX Studio获取端口号连接PowerBI
EXCEL通过DAX Studio获取端口号连接PowerBI 昨天我分享了EXCEL链接模板是通过获取端口号和数据库来连接PowerBI模型的,链接:浅析EXCEL自动连接PowerBI的模板,而DAX Studio可以获取处于打开状态的PowerBI的端口号。 以一个案例分享如何EXCEL…...
PostgreSQL 技术峰会,为您打造深度交流优质平台
峰会背景 PostgreSQL 作为全球领先的开源关系型数据库管理系统,凭借其强大的功能、高度的扩展性和稳定性,在云计算、大数据、人工智能等领域得到了广泛应用。随着数字化转型的加速,企业对数据库技术的需求日益复杂和多样化,Postg…...
使用 OpenCV (C++) 进行人脸边缘提取
使用 OpenCV (C) 进行人脸边缘提取 本文将介绍如何使用 C 和 OpenCV 库来检测图像中的人脸,并提取这些区域的边缘。我们将首先使用 Haar级联分类器进行人脸检测,然后在检测到的人脸区域(ROI - Region of Interest)内应用 Canny 边…...

C# 委托UI控件更新例子,何时需要使用委托
1. 例子1 private void UdpRxCallBackFunc(UdpDataStruct info) {// 1. 前置检查防止无效调用if (textBoxOutput2.IsDisposed || !textBoxOutput2.IsHandleCreated)return;// 2. 使用正确的委托类型Invoke(new Action(() >{// 3. 双重检查确保安全if (textBoxOutput2.IsDis…...

大模型数据流处理实战:Vue+NDJSON的Markdown安全渲染架构
在Vue中使用HTTP流接收大模型NDJSON数据并安全渲染 在构建现代Web应用时,处理大模型返回的流式数据并安全地渲染到页面是一个常见需求。本文将介绍如何在Vue应用中通过普通HTTP流接收NDJSON格式的大模型响应,使用marked、highlight.js和DOMPurify等库进…...
python项目如何创建docker环境
这里写自定义目录标题 python项目创建docker环境docker配置国内镜像源构建一个Docker 镜像验证镜像合理的创建标题,有助于目录的生成如何改变文本的样式插入链接与图片如何插入一段漂亮的代码片生成一个适合你的列表创建一个表格设定内容居中、居左、居右SmartyPant…...
Eureka 高可用集群搭建实战:服务注册与发现的底层原理与避坑指南
引言:为什么 Eureka 依然是存量系统的核心? 尽管 Nacos 等新注册中心崛起,但金融、电力等保守行业仍有大量系统运行在 Eureka 上。理解其高可用设计与自我保护机制,是保障分布式系统稳定的必修课。本文将手把手带你搭建生产级 Eur…...

PyTorch--池化层(4)
池化层(Pooling Layer) 用于降低特征图的空间维度,减少计算量和参数数量,同时保留最重要的特征信息。 池化作用:比如1080p视频——720p 池化层的步长默认是卷积核的大小 ceil 允许有出界部分;floor 不允许…...
GPU加速与非加速的深度学习张量计算对比Demo,使用PyTorch展示关键差异
import torch import time # 创建大型随机张量 (10000x10000) tensor_size 10000 x_cpu torch.randn(tensor_size, tensor_size) x_gpu x_cpu.cuda() # 转移到GPU # CPU矩阵乘法 start time.time() result_cpu torch.mm(x_cpu, x_cpu.t()) cpu_time time.time() - sta…...
Vue中的自定义事件
一、前言 在 Vue 的组件化开发中,组件之间的数据通信是构建复杂应用的关键。而其中最常见、最推荐的方式之一就是通过 自定义事件(Custom Events) 来实现父子组件之间的交互。 本文将带你深入了解: Vue 中事件的基本概念如何在…...

2025年大模型平台落地实践研究报告|附75页PDF文件下载
本报告旨在为各行业企业在建设落地大模型平台的过程中,提供有效的参考和指引,助力大模型更高效更有价值地规模化落地。本报告系统性梳理了大模型平台的发展背景、历程和现状,结合大模型平台的特点提出了具体的落地策略与路径,同时…...

PPTAGENT:让PPT生成更智能
想要掌握如何将大模型的力量发挥到极致吗?叶梓老师带您深入了解 Llama Factory —— 一款革命性的大模型微调工具。 1小时实战课程,您将学习到如何轻松上手并有效利用 Llama Factory 来微调您的模型,以发挥其最大潜力。 CSDN教学平台录播地址…...