【Hot100】11.贪心算法
这一类的核心思想是:在每一步选择中都采取在当前状态下最好/最优的选择(局部最优),从而希望导致结果是全局最优的。
- 适用场景:
- 问题可以分解为多个步骤,且每一步的选择互不干扰(或干扰可被局部最优覆盖)。
- 具有“贪心选择性质”和“最优子结构”。
- 常见于:区间问题、分配问题、跳跃问题、霍夫曼编码等。
- 解题关键:
- 证明难,直觉准:贪心算法往往很难严格证明,但在面试中,通常通过“反证法”或“举反例”来验证直觉。如果找不到反例,且逻辑自洽,通常就是贪心。
- 维护关键变量:如
maxReach(最远可达位置)、end(当前段结束位置)、lastPos(字符最后出现位置)等。
📝 本类题型总结
| 题目 | 核心技巧 | 关键差异点 (vs 跳跃游戏) |
|---|---|---|
| 55. 跳跃游戏 | 最远可达范围 | 基准:维护 maxReach,判断 i > maxReach。 |
| 45. 跳跃游戏 II | 边界计数 | 增加 end 边界。i == end 时步数 +1,更新 end = maxReach。求最小步数。 |
| 763. 划分字母区间 | 最后位置 + 边界 | 预处理 lastPos。end = max(end, lastPos)。i == end 时切割。求片段长度。 |
🏆 母题:LeetCode 55. 跳跃游戏 (Jump Game)
题目内容:给你一个非负整数数组 nums ,你最初位于数组的 第一个下标 。数组中的每个元素代表你在该位置可以跳跃的最大长度。判断你是否能够到达最后一个下标。
核心逻辑:
- 局部最优:在每一步,都尝试跳到能跳到的最远位置。
- 全局判断:维护一个
maxReach变量,表示当前能到达的最远下标。 - 失败条件:如果遍历到某个下标
i,而i > maxReach,说明前面的所有跳跃都无法到达i,直接返回false。 - 成功条件:如果
maxReach >= 最后一个下标,返回true。
class Solution {
public boolean canJump(int[] nums) {
int maxReach = 0; // ⚠️ [核心变量] 当前能到达的最远下标
for (int i = 0; i < nums.length; i++) {
// ⚠️ [核心逻辑 1] 如果当前位置超过了最远可达范围,说明无法到达
if (i > maxReach) {
return false;
}
// ⚠️ [核心逻辑 2] 更新最远可达范围
maxReach = Math.max(maxReach, i + nums[i]);
// ⚠️ [剪枝] 如果已经能到达或超过终点,提前返回
if (maxReach >= nums.length - 1) {
return true;
}
}
return true;
}
}
🔄 变体 1:LeetCode 45. 跳跃游戏 II (Jump Game II)
题目内容:给定一个长度为 n 的非负整数数组 nums ,你最初位于数组的第一个位置。数组中的每个元素代表你在该位置可以跳跃的最大长度。你的目标是使用最少的跳跃次数到达数组的最后一个位置。
相比母题的变化:
- 目标变化:从“能否到达”变为“最少步数”。
- 边界控制:
maxReach:当前能跳到的最远位置(同母题)。end:当前这一步能跳到的最远边界。steps:跳跃次数。
- 贪心策略:
- 在
[start, end]范围内遍历,不断更新maxReach。 - 当遍历到
i == end时(说明当前这一步的所有可能性都探索完了),必须进行一次跳跃,步数steps++,并将end更新为maxReach(进入下一步的范围)。 - 注意:不需要遍历到最后一个元素,因为到达最后一个元素前的边界时,就已经确定能到了。
- 在
class Solution {
public int jump(int[] nums) {
if (nums.length <= 1) return 0;
int steps = 0;
int maxReach = 0;
int end = 0; // ⚠️ [变化点 1] 当前步数能到达的边界
// ⚠️ [变化点 2] 不需要遍历到最后一个元素,因为到达倒数第二个元素的边界时,下一步肯定能到终点
for (int i = 0; i < nums.length - 1; i++) {
maxReach = Math.max(maxReach, i + nums[i]);
// ⚠️ [变化点 3] 到达当前步的边界,必须跳跃
if (i == end) {
steps++;
end = maxReach; // 更新边界为下一步能到的最远位置
// 优化:如果更新后的边界已经覆盖终点,可以直接返回
if (end >= nums.length - 1) {
return steps;
}
}
}
return steps;
}
}
🔄 变体 2:LeetCode 763. 划分字母区间 (Partition Labels)
题目内容:给你一个字符串 s 。我们要把这个字符串划分为尽可能多的片段,同一字母最多出现在一个片段中。返回一个表示每个字符串片段的长度的列表。
相比母题的变化:
- 预处理:需要先遍历一次字符串,记录每个字符最后出现的位置
lastPos[char]。 - 贪心策略:
- 遍历字符串,维护当前片段的结束位置
end。 - 对于当前字符
c,其最后出现位置是lastPos[c]。为了保证c不出现在其他片段,当前片段必须至少延伸到lastPos[c]。 - 所以:
end = max(end, lastPos[c])。 - 分割点:当遍历下标
i == end时,说明当前片段内所有字符的最后出现位置都在i之前(或就是i),可以安全切割。
- 遍历字符串,维护当前片段的结束位置
import java.util.ArrayList;
import java.util.List;
class Solution {
public List<Integer> partitionLabels(String s) {
List<Integer> result = new ArrayList<>();
int[] lastPos = new int[26];
// ⚠️ [变化点 1] 预处理:记录每个字符最后出现的位置
for (int i = 0; i < s.length(); i++) {
lastPos[s.charAt(i) - 'a'] = i;
}
int start = 0;
int end = 0;
for (int i = 0; i < s.length(); i++) {
char c = s.charAt(i);
// ⚠️ [变化点 2] 更新当前片段必须延伸到的最远位置
end = Math.max(end, lastPos[c - 'a']);
// ⚠️ [变化点 3] 贪心分割:当前位置等于最远位置,说明可以切割
if (i == end) {
result.add(end - start + 1);
start = i + 1; // 更新下一片段的起点
}
}
return result;
}
}

浙公网安备 33010602011771号