

🔥个人主页:代码不加冰(欢迎来访)
🎬作者简介:java后端学习者
❄️个人专栏:LeetCode刷题日记 ,苍穹外卖日记,SSM框架深入,JavaWeb,
✨命运的结局尽可永在,不屈的挑战却不可须臾或缺!
前言:
大家好,我是代码不加冰,经过一段时间的学习,我们已经学完了回溯算法相关的章节,可能刚开始的时候学的很慢,有时候一个多小时才能写完,不过这都是很正常的,没必要去否定自己,多复盘,多写几遍就可以了,我们接下来进入到贪心算法的章节,我们先介绍一下贪心算法,然后通过一个简单的题目来正式的进入到贪心算法 的专题。
摘要:
本文系统介绍了贪心算法的核心思想与典型应用场景。贪心算法通过局部最优选择 逐步构建全局解,其两大核心为贪心选择性质 和最优子结构。与动态规划相比,贪心算法效率更高(如O(nlogn)),但需严格证明策略的正确性。经典应用包括霍夫曼编码、最小生成树算法等。文章以**分发饼干(LeetCode 455)**为例,演示贪心策略:排序后优先用大饼干满足大胃口,通过单次遍历实现O(nlogn)时间复杂度。最后强调贪心法的局限性(如某些找零问题需动态规划)及验证方法(反例法、数学归纳)。
什么是贪心算法:
贪心算法(Greedy Algorithm)是算法中一种非常直观且高效的方法。它的核心思想是:在对问题求解时,总是做出在当前看来最好的选择。 换句话说,贪心算法不从整体最优上加以考虑,它所做出的仅仅是局部最优解 。虽然贪心算法不能对所有问题都得到整体最优解,但对许多特定问题,局部最优的选择最终能够导致整体最优。
核心理论基础
我们要掌握贪心算法,必须理解它的两个核心特质:
1. 贪心选择性质 (Greedy Choice Property)
一个全局最优解可以通过一系列局部最优(贪心)的选择来达到。也就是说,在考虑当前步骤时,我们直接做出当前利益最大的选择,而不必考虑子问题的后续结果 。这就是"走一步看一步,每步挑最好"。
2. 最优子结构 (Optimal Substructure)
如果一个问题的最优解包含其子问题的最优解,那么这个问题就具有最优子结构。贪心算法和动态规划(DP)都要求问题具备这个性质。
贪心算法的四大步骤
在实际做题或解决问题时,通常遵循以下步骤:
-
将问题分解:把大问题分解为若干个子问题(步骤)。
-
找出贪心策略 :确定每一步的局部最优解是什么(这一步最难,需要逻辑推理或直觉)。
-
求解局部最优:每一步都采取这个贪心策略,得到阶段性结果。
-
堆叠成全局最优 :将所有的局部最优解合并,转化为整个问题的解。
贪心法 vs 动态规划
很多人容易混淆贪心和动态规划,它们的核心区别在于"是否回头":
| 特性 | 贪心算法 (Greedy) | 动态规划 (DP) |
|---|---|---|
| 选择方式 | 每一步做出当前最优选择,不能回溯(不后悔)。 | 综合所有子问题的解做出选择,有状态转移。 |
| 子问题依赖 | 当前的选择不依赖于未解决的子问题。 | 当前的选择依赖于子问题的解(需要查表)。 |
| 效率 | 极高(通常是 O(n \log n)$或 O(n)。 | 较高(通常是 O(n^2) 或 O(n \cdot W) )。 |
| 正确性 | 需要严格证明(不是所有问题都能用)。 | 只要状态转移方程正确,必然得到最优解。 |
经典应用场景
贪心算法在计算机科学中有非常多著名的经典应用:
-
霍夫曼编码(Huffman Coding):每次合并频次最低的两棵树,实现最优压缩。
-
普里姆算法(Prim)& 克鲁斯卡尔算法(Kruskal):求解图的最小生成树(MST),每次都选权值最小的边。
-
迪杰斯特拉算法(Dijkstra):求解单源最短路径,每次选择距离源点最近的未访问节点。
-
区间调度问题:给定多个活动和它们的开始/结束时间,如何安排能参加最多的活动(策略:每次选结束时间最早的活动)。
贪心算法的致命弱点
贪心算法最大的难点不在于代码编写,而在于"你怎么知道你的贪心策略是对的"。很多时候,主观直觉上的"最优"往往是错的。
举个栗子:找零钱问题
- 假设币值有
[25, 10, 1]分。如果要找 41 分,贪心策略(每次选最大的):25 + 10 + 1 + 1 + 1 + 1 + 1(7张),但实际最优解是25 + 8(不存在8),或者如果有[25, 20, 1]找 40 分,贪心是25 + 1*15(16张),而实际最优是20 + 20(2张)。此时贪心失效,必须用动态规划。
常见的证明方法:
-
数学归纳法:证明第一步贪心是对的,并证明如果前 k 步是对的,第 k+1步也是对的。
-
反证法(替换法):假设存在一个比贪心解更优的"神仙解",然后证明可以用贪心选择逐步替换掉这个"神仙解"里的选择,而结果不会变差。
刷题小技巧 :在算法面试或竞赛中,如果难以用数学严谨证明,可以通过举反例。如果试了几个极端的边界情况都找不到反例,那么这个贪心策略大概率是可行的。
具体题目:455.分发饼干
假设你是一位很棒的家长,想要给你的孩子们一些小饼干。但是,每个孩子最多只能给一块饼干。
对每个孩子
i,都有一个胃口值g[i],这是能让孩子们满足胃口的饼干的最小尺寸;并且每块饼干j,都有一个尺寸s[j]。如果s[j] >= g[i],我们可以将这个饼干j分配给孩子i,这个孩子会得到满足。你的目标是满足尽可能多的孩子,并输出这个最大数值。示例 1:
输入: g = [1,2,3], s = [1,1] 输出: 1 解释: 你有三个孩子和两块小饼干,3 个孩子的胃口值分别是:1,2,3。 虽然你有两块小饼干,由于他们的尺寸都是 1,你只能让胃口值是 1 的孩子满足。 所以你应该输出 1。示例 2:
输入: g = [1,2], s = [1,2,3] 输出: 2 解释: 你有两个孩子和三块小饼干,2 个孩子的胃口值分别是 1,2。 你拥有的饼干数量和尺寸都足以让所有孩子满足。 所以你应该输出 2。提示:
1 <= g.length <= 3 * 1040 <= s.length <= 3 * 1041 <= g[i], s[j] <= 231 - 1
题目分析:
其实这道题就是很简单的逻辑,感觉像是小学还是幼儿园的逻辑

目的就是大的饼干给胃口大的,小的饼干给胃口小的,但是要有限制,大的饼干要能满足胃口大的,否则也是不合规的。
根据步骤来:
这里的局部最优就是大饼干喂给胃口大的,充分利用饼干尺寸喂饱一个,全局最优就是喂饱尽可能多的小孩。
可以尝试使用贪心策略,先将饼干数组和小孩数组排序。
然后从后向前遍历小孩数组,用大饼干优先满足胃口大的,并统计满足小孩数量

不过要注意代码细节,很多人可能上来想着就是用两个for循环来分别遍历,其实是多余的,我们只需要用索引来替代一下,毕竟要比较的是值。
题目答案:
class Solution { // 思路2:优先考虑胃口,先喂饱大胃口 public int findContentChildren(int[] g, int[] s) { Arrays.sort(g); Arrays.sort(s); int count = 0; int start = s.length - 1; // 遍历胃口 for (int index = g.length - 1; index >= 0; index--) { if(start >= 0 && g[index] <= s[start]) { start--; count++; } } return count; } }