数据结构与算法|第十七章:贪心算法
- [第十七章 贪心算法](#第十七章 贪心算法)
-
- [17.1 贪心算法的核心思想与证明方法](#17.1 贪心算法的核心思想与证明方法)
-
- [17.1.1 核心思想](#17.1.1 核心思想)
- [17.1.2 贪心算法的证明方法](#17.1.2 贪心算法的证明方法)
-
- [1. 归纳法(Induction)](#1. 归纳法(Induction))
- [2. 交换论证法(Exchange Argument)](#2. 交换论证法(Exchange Argument))
- [3. 拟阵理论(Matroid Theory)](#3. 拟阵理论(Matroid Theory))
- [17.2 贪心 vs 动态规划的适用场景对比](#17.2 贪心 vs 动态规划的适用场景对比)
-
- [17.2.1 核心区别](#17.2.1 核心区别)
- [17.2.2 适用场景对比](#17.2.2 适用场景对比)
- [17.2.3 判断标准:"贪心选择性质"](#17.2.3 判断标准:"贪心选择性质")
- [17.3 经典实战](#17.3 经典实战)
-
- [17.3.1 活动选择问题(Activity Selection)](#17.3.1 活动选择问题(Activity Selection))
- [17.3.2 分数背包问题(Fractional Knapsack)](#17.3.2 分数背包问题(Fractional Knapsack))
- [17.3.3 哈夫曼编码(Huffman Coding)](#17.3.3 哈夫曼编码(Huffman Coding))
- [17.3.4 区间调度问题(合并区间)](#17.3.4 区间调度问题(合并区间))
- [17.3.5 分发糖果(Candy)](#17.3.5 分发糖果(Candy))
- [17.3.6 跳跃游戏 I & II(Jump Game)](#17.3.6 跳跃游戏 I & II(Jump Game))
-
- [跳跃游戏 I(能否到达终点)](#跳跃游戏 I(能否到达终点))
- [跳跃游戏 II(最少跳跃次数)](#跳跃游戏 II(最少跳跃次数))
- 总结与预告
上篇:第十六章、二分查找与搜索
第十七章 贪心算法
在上一章中,我们学习了二分查找------一种"每次排除一半"的搜索策略。从本章开始,我们将正式进入算法设计 的核心领域。而第一个登场的,就是最具"直觉魅力"的算法策略:贪心算法(Greedy Algorithm)。
贪心算法:在每一步决策时,都选择当前看起来最优 的选择,期望通过一系列局部最优决策,最终得到全局最优解。
它的吸引力在于------思路直接、实现简单、时间复杂度通常很低。但它的陷阱也在于------局部最优不等于全局最优。贪心算法能工作的场景,必须满足特定的"贪心选择性质"和"最优子结构"。
本章将从贪心的核心思想与证明方法出发,通过贪心与动态规划的对比帮你判断"何时用贪心",最后用六大经典实战让你彻底掌握这一策略。
17.1 贪心算法的核心思想与证明方法
17.1.1 核心思想
贪心算法的核心流程可以概括为:
否
是
初始问题
当前步骤
在所有可选方案中
选择局部最优解
缩小问题规模
问题是否已解决?
得到最终解
贪心算法不回溯:一旦做出选择,就不会再更改。这是它与回溯算法、动态规划最本质的区别------贪心没有"后悔药"。
举一个生活化的例子:假设你在超市结账,收银员找零 63 元,纸币面额有 {20, 10, 5, 1}。收银员会怎么做?
- 先拿最大面额 20:63 − 20 × 3 = 3(3 张 20 元)
- 再拿 5?不行,3 < 5,跳过
- 拿 1:3 − 1 × 3 = 0(3 张 1 元)
- 结果:3 张 20 + 3 张 1 = 6 张纸币
这就是贪心------每一步都选面额最大的、不超过剩余金额的纸币。在这个面额体系下,这确实是最优解(最少张数)。但如果面额变成 {20, 15, 1},找零 30 元呢?
- 贪心:20 + 1 × 10 = 11 张
- 最优:15 + 15 = 2 张
这就是贪心失效的例子------贪心选择不一定总是全局最优。
17.1.2 贪心算法的证明方法
要让贪心算法站得住脚,必须经过严格的数学证明。以下是三种主流证明方法:
1. 归纳法(Induction)
用数学归纳法证明:贪心策略在每一步做出的选择,不会导致后续无法得到最优解。
证明框架:假设贪心策略在前 k 步是正确的,证明第 k+1 步的贪心选择仍然保持最优性。通常需要配合"最优子结构"一起使用。
适用场景:大多数贪心问题都可以用归纳法证明,尤其是"每次选最小/最大"这类问题。
2. 交换论证法(Exchange Argument)
核心思想 :假设存在一个全局最优解 O,如果 O 的第一步选择与贪心策略 G 的第一步不同,我们可以交换某些元素,将 O "改造"成另一个最优解 O′,使得 O′ 的第一步与 G 一致,并且不损失最优性。通过反复交换,最终将任意最优解"对齐"到贪心解。
这是贪心算法最强有力的证明武器,尤其适用于活动选择、任务调度等"选与不选"类问题。
3. 拟阵理论(Matroid Theory)
如果一个组合优化问题可以建模为 拟阵(Matroid) 上的优化问题,那么贪心算法一定能得到最优解。拟阵是一个抽象的代数结构,定义了"独立集"的概念。
适用场景:最小生成树(Kruskal 算法)、最大权独立集等。这是贪心算法正确性的"终极解释",但门槛较高,面试和实际开发中较少用到。
17.2 贪心 vs 动态规划的适用场景对比
贪心和动态规划(DP)经常被放在一起比较,因为它们都依赖 "最优子结构"。但两者的决策方式截然不同。
17.2.1 核心区别
| 对比维度 | 贪心算法 | 动态规划 |
|---|---|---|
| 决策方式 | 每步做出一个不可撤销的局部最优选择 | 记录所有子问题的解,通过状态转移推导 |
| 回溯能力 | 不能回退,选了就不能改 | 可以回退,通过状态表保存所有可能 |
| 解空间探索 | 只走一条路(贪心选择那条) | 遍历所有可能的子问题 |
| 时间复杂度 | 通常 O(n log n) 或更低 | 通常 O(n²) 或更高 |
| 正确性保证 | 需要严格的数学证明 | 状态转移方程推导即可 |
| 典型问题 | 活动选择、哈夫曼编码、最小生成树 | 背包问题(0-1)、编辑距离、LCS |
17.2.2 适用场景对比
否
是
是
否
是
否
给定一个问题
问题是否具有
最优子结构?
贪心和DP都无法保证最优解
考虑回溯/暴力搜索
是否具有
贪心选择性质?
贪心算法 ✓
O n log n 或更低
子问题是否
大量重叠?
动态规划 ✓
O n² 或更高
分治法 ✓
如归并排序、快速排序
17.2.3 判断标准:"贪心选择性质"
贪心选择性质(Greedy Choice Property) :全局最优解可以通过一系列局部最优选择 得到。或者说,贪心的第一步选择,一定存在于某个全局最优解中。
快速判断法:
- 如果"排序后按某种顺序贪心"能通过直觉验证 → 大概率可行,但需要严格证明
- 如果问题中存在"选了 A 就不能选 B,且 A 和 B 之间有复杂依赖" → 大概率是 DP
- 如果问题问的是"最大/最小值"且选择之间无后效性 → 先尝试贪心,再用反例验证
17.3 经典实战
17.3.1 活动选择问题(Activity Selection)
问题描述 :给定 n 个活动,每个活动 i i i 有开始时间 s i s_i si 和结束时间 f i f_i fi( s i < f i s_i < f_i si<fi)。同一时间只能参加一个活动。求最多能参加多少个互不重叠的活动。
示例:
活动: A B C D E F
开始: 1 3 0 5 8 5
结束: 4 5 6 7 9 9
贪心策略:每次选择结束时间最早的活动。
直觉解释:结束得越早,留给后面活动的时间就越多,就越有机会安排更多活动。每次都选"最不贪心占用时间"的那个,最终反而能得到最多的数量。
交换论证证明概要 :假设全局最优解 O 不是按最早结束时间选的。设 O 的第一个活动为 a k a_k ak,贪心策略 G 的第一个活动为 a 1 a_1 a1( f 1 ≤ f k f_1 \le f_k f1≤fk)。用 a 1 a_1 a1 替换 a k a_k ak 得到 O ′ O' O′:因为 f 1 ≤ f k f_1 \le f_k f1≤fk, a 1 a_1 a1 不会和 O 中的后续活动冲突,所以 O ′ O' O′ 仍是可行解且活动数相同,仍是最优解。重复此过程,可将任意最优解逐步"对齐"到贪心解。
java
import java.util.Arrays;
import java.util.Comparator;
/**
* 活动选择问题 ------ 贪心算法
* 策略:按结束时间升序排序,依次选择不冲突的活动
* 时间复杂度:O(n log n)(排序)
* 空间复杂度:O(n)(存储结果)
*/
public class ActivitySelection {
/**
* 活动类
*/
static class Activity {
int start;
int finish;
Activity(int start, int finish) {
this.start = start;
this.finish = finish;
}
@Override
public String toString() {
return "(" + start + ", " + finish + ")";
}
}
/**
* 选择最大数量的互不重叠活动
* @param activities 活动数组
* @return 最多可选的活动数量
*/
public int selectMaxActivities(Activity[] activities) {
if (activities == null || activities.length == 0) return 0;
// 1. 按结束时间升序排序
Arrays.sort(activities, Comparator.comparingInt(a -> a.finish));
// 2. 贪心选择:第一个活动必选
int count = 1;
int lastFinish = activities[0].finish;
// 3. 遍历剩余活动
for (int i = 1; i < activities.length; i++) {
if (activities[i].start >= lastFinish) {
count++;
lastFinish = activities[i].finish;
}
}
return count;
}
/**
* 返回具体选择的活动列表
* @param activities 活动数组
* @return 被选中的活动数组
*/
public Activity[] getSelectedActivities(Activity[] activities) {
if (activities == null || activities.length == 0) return new Activity[0];
Arrays.sort(activities, Comparator.comparingInt(a -> a.finish));
// 先统计数量
int count = 1;
int lastFinish = activities[0].finish;
for (int i = 1; i < activities.length; i++) {
if (activities[i].start >= lastFinish) {
count++;
lastFinish = activities[i].finish;
}
}
// 再收集结果
Activity[] result = new Activity[count];
int idx = 0;
result[idx++] = activities[0];
lastFinish = activities[0].finish;
for (int i = 1; i < activities.length; i++) {
if (activities[i].start >= lastFinish) {
result[idx++] = activities[i];
lastFinish = activities[i].finish;
}
}
return result;
}
}
复杂度分析 :排序 O(n log n),遍历 O(n),总 O(n log n)。空间复杂度 O(n)(排序栈 + 结果数组)。
17.3.2 分数背包问题(Fractional Knapsack)
问题描述 :给定 n 个物品,每个物品 i i i 有重量 w i w_i wi 和价值 v i v_i vi。背包容量为 W。物品可以被分割(取任意比例)。求能装入背包的最大总价值。
与 0-1 背包的区别:0-1 背包中物品不可分割,必须用 DP;分数背包中物品可分割,贪心即最优。
贪心策略:按单位重量价值( v i / w i v_i / w_i vi/wi)降序排列,优先拿"性价比"最高的物品,拿完为止。
示例:W = 50
物品 A:重量 10,价值 60 → 性价比 6
物品 B:重量 20,价值 100 → 性价比 5
物品 C:重量 30,价值 120 → 性价比 4
贪心:先拿 A(10,价值 60),再拿 B(20,价值 100),剩余容量 20,拿 C 的 20/30 → 价值 80
总价值 = 60 + 100 + 80 = 240
正确性证明 :假设最优解没有按性价比排序。设最优解中拿了物品 i i i 和 j j j,但 i i i 性价比 < j j j 性价比。如果将 i i i 的微小重量 ε \varepsilon ε 换成 j j j(即少拿一点 i i i,多拿一点 j j j),总价值必然增加,与"最优"矛盾。因此最优解一定按性价比降序取物品。
java
import java.util.Arrays;
import java.util.Comparator;
/**
* 分数背包问题 ------ 贪心算法
* 策略:按单位重量价值降序,依次取物品直至背包装满
* 时间复杂度:O(n log n)
* 空间复杂度:O(1)(原地排序)
*/
public class FractionalKnapsack {
/**
* 物品类
*/
static class Item {
int weight;
int value;
/** 单位重量价值(性价比) */
double ratio;
Item(int weight, int value) {
this.weight = weight;
this.value = value;
this.ratio = (double) value / weight;
}
}
/**
* 计算分数背包的最大总价值
* @param items 物品数组
* @param capacity 背包容量
* @return 最大总价值
*/
public double getMaxValue(Item[] items, int capacity) {
if (items == null || items.length == 0 || capacity <= 0) return 0.0;
// 1. 按性价比降序排序
Arrays.sort(items, Comparator.comparingDouble(a -> -a.ratio));
double totalValue = 0.0;
int remainingCapacity = capacity;
// 2. 贪心:优先拿性价比最高的
for (Item item : items) {
if (remainingCapacity <= 0) break;
if (item.weight <= remainingCapacity) {
// 整个物品都能装入
totalValue += item.value;
remainingCapacity -= item.weight;
} else {
// 只能装入部分
double fraction = (double) remainingCapacity / item.weight;
totalValue += item.value * fraction;
remainingCapacity = 0;
}
}
return totalValue;
}
}
复杂度分析 :排序 O(n log n),遍历 O(n),总 O(n log n)。空间复杂度 O(1)。
17.3.3 哈夫曼编码(Huffman Coding)
问题描述 :给定 n 个字符及其出现频率,构造一棵最优前缀编码树(哈夫曼树),使得编码后的总比特数最小。前缀编码要求:没有任何一个字符的编码是另一个字符编码的前缀。
贪心策略:使用优先队列(最小堆),每次取出两个频率最小的节点合并成一个新节点,新节点的频率为两者之和,再放回堆中。重复直到只剩一个根节点。
示例:字符 a(5), b(9), c(12), d(13), e(16), f(45)
步骤:
1. 取 5+9=14 → 堆: {12, 13, 14, 16, 45}
2. 取 12+13=25 → 堆: {14, 16, 25, 45}
3. 取 14+16=30 → 堆: {25, 30, 45}
4. 取 25+30=55 → 堆: {45, 55}
5. 取 45+55=100 → 堆: {100} 完成!
最终哈夫曼树结构:
0
1
0
1
0
1
0
1
0
1
root (100)
f:45
55
25
30
c:12
d:13
14
e:16
a:5
b:9
编码结果(左0右1):
f: 0
c: 100
d: 101
a: 1100
b: 1101
e: 111
正确性证明(交换论证):设 x 和 y 是频率最小的两个字符。假设最优树 T 中 x 和 y 不是最深的兄弟节点。将最深的一对兄弟叶子与 x、y 交换,由于 x、y 频率最小,交换后总代价不会增加,仍是最优解。由此证明贪心选择正确。
java
import java.util.PriorityQueue;
/**
* 哈夫曼编码 ------ 贪心算法
* 策略:使用最小堆,每次合并频率最小的两个节点
* 时间复杂度:O(n log n)
* 空间复杂度:O(n)
*/
public class HuffmanCoding {
/**
* 哈夫曼树节点
*/
static class HuffmanNode implements Comparable<HuffmanNode> {
char ch; // 字符(叶子节点有效)
int freq; // 频率
HuffmanNode left; // 左子树(编码 0)
HuffmanNode right; // 右子树(编码 1)
HuffmanNode(char ch, int freq) {
this.ch = ch;
this.freq = freq;
}
HuffmanNode(int freq, HuffmanNode left, HuffmanNode right) {
this.ch = '\0'; // 内部节点无字符
this.freq = freq;
this.left = left;
this.right = right;
}
boolean isLeaf() {
return left == null && right == null;
}
@Override
public int compareTo(HuffmanNode other) {
return Integer.compare(this.freq, other.freq);
}
}
/**
* 构建哈夫曼树
* @param chars 字符数组
* @param freqs 对应频率数组
* @return 哈夫曼树的根节点
*/
public HuffmanNode buildHuffmanTree(char[] chars, int[] freqs) {
if (chars == null || freqs == null || chars.length != freqs.length) {
throw new IllegalArgumentException("chars 和 freqs 长度必须相等且非空");
}
// 1. 将所有字符加入最小堆
PriorityQueue<HuffmanNode> minHeap = new PriorityQueue<>();
for (int i = 0; i < chars.length; i++) {
minHeap.offer(new HuffmanNode(chars[i], freqs[i]));
}
// 2. 贪心合并:每次取两个频率最小的节点
while (minHeap.size() > 1) {
HuffmanNode left = minHeap.poll();
HuffmanNode right = minHeap.poll();
HuffmanNode parent = new HuffmanNode(
left.freq + right.freq, left, right
);
minHeap.offer(parent);
}
// 3. 返回根节点
return minHeap.poll();
}
/**
* 打印每个字符的哈夫曼编码
* @param root 哈夫曼树根节点
* @param code 当前路径编码(递归参数)
*/
public void printCodes(HuffmanNode root, String code) {
if (root == null) return;
if (root.isLeaf()) {
System.out.println(root.ch + ": " + code);
return;
}
printCodes(root.left, code + "0");
printCodes(root.right, code + "1");
}
/**
* 计算 WPL(带权路径长度)= 总编码比特数
* @param root 哈夫曼树根节点
* @param depth 当前深度
* @return WPL 值
*/
public int computeWPL(HuffmanNode root, int depth) {
if (root == null) return 0;
if (root.isLeaf()) {
return root.freq * depth;
}
return computeWPL(root.left, depth + 1)
+ computeWPL(root.right, depth + 1);
}
}
复杂度分析 :建堆 O(n),每次 poll 和 offer 都是 O(log n),共执行 n−1 次合并,总 O(n log n)。空间复杂度 O(n)。
17.3.4 区间调度问题(合并区间)
问题描述(LeetCode 56) :给定若干区间
intervals[i] = [start_i, end_i],将所有重叠的区间合并,返回合并后的区间数组。
输入:[[1,3], [2,6], [8,10], [15,18]]
输出:[[1,6], [8,10], [15,18]]
解释:[1,3] 和 [2,6] 重叠 → 合并为 [1,6]
贪心策略:按区间起点升序排序,然后依次遍历。若当前区间与上一个合并区间重叠(curr[0] <= last[1]),则扩展 last[1] = max(last[1], curr[1]);否则,当前区间作为一个新的独立区间加入结果。
java
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
/**
* 合并区间 ------ 贪心算法
* 策略:按起点排序,依次判断重叠并合并
* 时间复杂度:O(n log n)
* 空间复杂度:O(n)(存储结果)
*/
public class MergeIntervals {
/**
* 合并所有重叠区间
* @param intervals 区间数组,每个区间为 [start, end]
* @return 合并后的区间数组
*/
public int[][] merge(int[][] intervals) {
if (intervals == null || intervals.length <= 1) {
return intervals;
}
// 1. 按起点升序排序
Arrays.sort(intervals, Comparator.comparingInt(a -> a[0]));
List<int[]> result = new ArrayList<>();
int[] last = intervals[0]; // 当前合并中的区间
// 2. 贪心合并
for (int i = 1; i < intervals.length; i++) {
int[] curr = intervals[i];
if (curr[0] <= last[1]) {
// 重叠:扩展终点
last[1] = Math.max(last[1], curr[1]);
} else {
// 不重叠:保存上一个,开始新区间
result.add(last);
last = curr;
}
}
result.add(last); // 最后一个区间
return result.toArray(new int[result.size()][]);
}
}
复杂度分析 :排序 O(n log n),遍历 O(n),总 O(n log n)。空间复杂度 O(n)。
17.3.5 分发糖果(Candy)
问题描述(LeetCode 135) :n 个孩子排成一排,每个孩子有一个评分
ratings[i]。你需要给每个孩子分发糖果,满足:
- 每个孩子至少 1 颗糖
- 相邻两个孩子中,评分更高 的孩子必须获得更多糖果
求最少需要多少颗糖果。
输入:[1, 0, 2]
输出:5(分法:2 + 1 + 2)
输入:[1, 2, 2]
输出:4(分法:1 + 2 + 1)
贪心策略:两趟扫描。
- 第一趟从左到右 :若
ratings[i] > ratings[i-1],则candies[i] = candies[i-1] + 1 - 第二趟从右到左 :若
ratings[i] > ratings[i+1],则candies[i] = max(candies[i], candies[i+1] + 1)
为什么需要两趟? 一次遍历只能保证一个方向上的约束。"评分高 → 糖多"是双向约束------左邻居和右邻居都要满足。第一趟保证了"比左边高"的约束,第二趟补上"比右边高"的约束(同时不破坏已满足的左边约束,因为是取 max)。
java
import java.util.Arrays;
/**
* 分发糖果 ------ 贪心算法(两趟扫描)
* 时间复杂度:O(n)
* 空间复杂度:O(n)
*/
public class Candy {
/**
* 计算最少需要的糖果总数
* @param ratings 每个孩子的评分
* @return 最少糖果总数
*/
public int candy(int[] ratings) {
if (ratings == null || ratings.length == 0) return 0;
int n = ratings.length;
int[] candies = new int[n];
// 每人至少 1 颗
Arrays.fill(candies, 1);
// 第一趟:从左到右,保证"比左边高"的约束
for (int i = 1; i < n; i++) {
if (ratings[i] > ratings[i - 1]) {
candies[i] = candies[i - 1] + 1;
}
}
// 第二趟:从右到左,保证"比右边高"的约束
for (int i = n - 2; i >= 0; i--) {
if (ratings[i] > ratings[i + 1]) {
candies[i] = Math.max(candies[i], candies[i + 1] + 1);
}
}
// 求和
int total = 0;
for (int c : candies) {
total += c;
}
return total;
}
}
复杂度分析 :两趟遍历 + 一次求和,总 O(n)。空间复杂度 O(n)(candies 数组)。可以优化为 O(1) 空间(利用"递增递减序列"一次遍历),但 O(n) 写法更清晰,面试中优先使用。
17.3.6 跳跃游戏 I & II(Jump Game)
跳跃游戏 I(能否到达终点)
问题描述(LeetCode 55) :给定非负整数数组
nums,你最初位于下标 0。每个元素nums[i]表示从位置 i 最多可以跳跃的长度。判断能否到达最后一个下标。
输入:[2, 3, 1, 1, 4]
输出:true(路径:0→1→4)
输入:[3, 2, 1, 0, 4]
输出:false(卡在位置 3,无法继续前进)
贪心策略:维护一个"最远可达位置" maxReach。遍历数组,若当前位置 ≤ maxReach,则更新 maxReach。一旦 maxReach ≥ 最后一个下标,说明可到达。

java
/**
* 跳跃游戏 I ------ 贪心算法
* 时间复杂度:O(n)
* 空间复杂度:O(1)
*/
public boolean canJump(int[] nums) {
if (nums == null || nums.length == 0) return false;
int maxReach = 0;
int n = nums.length;
for (int i = 0; i < n; i++) {
// 当前位置不可达
if (i > maxReach) {
return false;
}
// 更新最远可达位置
maxReach = Math.max(maxReach, i + nums[i]);
// 提前终止:已经能到终点
if (maxReach >= n - 1) {
return true;
}
}
return true;
}
跳跃游戏 II(最少跳跃次数)
问题描述(LeetCode 45) :给定非负整数数组
nums,保证一定能到达最后一个下标。求最少跳跃次数。
输入:[2, 3, 1, 1, 4]
输出:2(0→1→4,共2跳)
贪心策略:BFS 式的贪心。维护三个变量:
jumps:已跳跃次数curEnd:当前跳跃能到达的最远边界curFarthest:在遍历过程中,下一跳能到达的最远位置
遍历到 i == curEnd 时,意味着当前跳跃范围已耗尽,必须再跳一次(jumps++),并将 curEnd 更新为 curFarthest。
直觉解释:想象你在做 BFS 层序遍历------每个"跳跃次数"对应 BFS 的一层。"这一跳"能到达的所有位置是当前层,"下一跳"能到达的新位置是下一层。
java
/**
* 跳跃游戏 II ------ 贪心算法(BFS 视角)
* 时间复杂度:O(n)
* 空间复杂度:O(1)
*/
public int jump(int[] nums) {
if (nums == null || nums.length <= 1) return 0;
int jumps = 0; // 跳跃次数
int curEnd = 0; // 当前跳跃能到达的最远位置
int curFarthest = 0; // 下一跳能到达的最远位置
int n = nums.length;
// 注意:只需遍历到 n-2,因为到达 n-1 就结束了
for (int i = 0; i < n - 1; i++) {
curFarthest = Math.max(curFarthest, i + nums[i]);
// 到达当前跳跃的边界,必须再跳一次
if (i == curEnd) {
jumps++;
curEnd = curFarthest;
// 提前终止
if (curEnd >= n - 1) {
break;
}
}
}
return jumps;
}
复杂度分析 :只遍历一次数组,O(n) 时间,O(1) 空间。
总结与预告
本章系统学习了贪心算法的完整体系:
- 17.1 核心思想与证明:贪心 = 每步选局部最优,不可回退。三种证明方法------归纳法(基础)、交换论证法(最常用)、拟阵理论(高级)
- 17.2 贪心 vs DP:贪心走一条路、不回退;DP 记所有路、可回退。判断标准是"贪心选择性质"是否存在
- 17.3 六大经典实战:
| 问题 | 贪心策略 | 时间复杂度 |
|---|---|---|
| 活动选择 | 按结束时间升序 | O(n log n) |
| 分数背包 | 按性价比降序 | O(n log n) |
| 哈夫曼编码 | 最小堆合并 | O(n log n) |
| 合并区间 | 按起点排序合并 | O(n log n) |
| 分发糖果 | 左右两趟扫描 | O(n) |
| 跳跃游戏 I & II | 维护最远可达位置/BFS 层级 | O(n) |
贪心算法的核心套路:
- 对数据进行排序(按某个维度)
- 按照排序后的顺序依次贪心选择
- 维护一个关键变量(如最大/最小值、最远距离、当前边界)
下一章我们将进入算法设计中的"重头戏"------动态规划。DP 是贪心算法失败时的"备选方案":当贪心选择性质不成立时,DP 通过穷举所有子问题来保证全局最优。我们将从斐波那契数列、爬楼梯等经典问题入手,学习 DP 的状态定义、状态转移方程和边界条件。
上篇:第十六章、二分查找与搜索