一、贪心算法核心知识点
1. 什么是贪心算法?
定义 :在每一步选择中都采取当前状态下最好或最优的选择,从而希望导致结果是全局最优的算法。
通俗理解:
-
就像吃自助餐,你每次都拿最贵的菜,希望最后吃回本
-
就像找零钱,每次都拿最大面额的纸币
2. 贪心算法的基本要素
| 要素 | 说明 |
|---|---|
| 贪心选择性质 | 每一步的局部最优选择能导致全局最优解 |
| 最优子结构 | 问题的最优解包含子问题的最优解 |
| 无后效性 | 当前决策不影响后续决策的独立性 |
3. 贪心 vs 动态规划
| 对比项 | 贪心算法 | 动态规划 |
|---|---|---|
| 决策方式 | 每一步只做当前最优 | 考虑所有可能,选全局最优 |
| 是否回溯 | 不回溯 | 需要回溯 |
| 时间复杂度 | 通常较低 | 通常较高 |
| 适用条件 | 贪心选择性质 + 最优子结构 | 最优子结构 + 重叠子问题 |
4. 贪心算法的步骤
text
1. 将问题分解为若干个子问题
2. 找出贪心策略(怎么选才是当前最优)
3. 证明贪心策略的正确性(最难)
4. 实现算法
二、经典贪心算法题目及代码
题目1:找零钱问题(LeetCode 322 变种)
问题:有面额 1, 2, 5, 10, 20, 50, 100 的纸币,用最少的张数凑出金额 n。
贪心策略:优先使用大面额纸币
python
def coin_change(coins, amount):
"""
贪心找零(前提:货币系统是标准的,能用贪心)
coins: 面额列表,已从大到小排序
amount: 需要凑的金额
返回:需要的纸币张数
"""
coins.sort(reverse=True) # 从大到小排序
count = 0
remaining = amount
for coin in coins:
if remaining == 0:
break
# 能用几张当前面额的纸币
num = remaining // coin
count += num
remaining -= num * coin
print(f"使用 {num} 张 {coin} 元")
if remaining != 0:
return -1 # 无法凑出
return count
# 测试
coins = [100, 50, 20, 10, 5, 2, 1]
amount = 188
result = coin_change(coins, amount)
print(f"最少需要 {result} 张纸币")
# 输出:使用 1 张 100元,1张50元,1张20元,1张10元,1张5元,1张2元,1张1元
题目2:活动选择问题
问题:给定多个活动的开始和结束时间,选择最多的互不冲突的活动。
贪心策略 :优先选择结束时间最早的活动
python
def activity_selection(activities):
"""
活动选择问题
activities: [(开始时间, 结束时间), ...]
返回:最多能选择的活动数量
"""
if not activities:
return 0
# 按结束时间排序
activities.sort(key=lambda x: x[1])
count = 1 # 选择第一个活动
last_end = activities[0][1] # 第一个活动的结束时间
selected = [activities[0]] # 记录选中的活动
for start, end in activities[1:]:
if start >= last_end: # 下一个活动的开始 >= 上一个活动的结束
count += 1
last_end = end
selected.append((start, end))
print("选中的活动:", selected)
return count
# 测试
activities = [(1, 4), (3, 5), (0, 6), (5, 7), (3, 9), (5, 9), (6, 10), (8, 11), (8, 12), (2, 14), (12, 16)]
result = activity_selection(activities)
print(f"最多能安排 {result} 个活动")
题目3:分发饼干(LeetCode 455)
问题:每个孩子最多只能给一块饼干。每个孩子 i 有一个胃口值 gi,每块饼干 j 有尺寸 sj。当 sj >= gi 时,孩子满足。最多能满足多少个孩子?
贪心策略 :用最小尺寸 的饼干去满足最小胃口的孩子
python
def find_content_children(g, s):
"""
分发饼干
g: 孩子的胃口值列表
s: 饼干尺寸列表
返回:最多能满足的孩子数量
"""
g.sort()
s.sort()
child = 0 # 孩子指针
cookie = 0 # 饼干指针
count = 0
while child < len(g) and cookie < len(s):
if s[cookie] >= g[child]:
# 这块饼干能满足这个孩子
count += 1
child += 1
cookie += 1
else:
# 这块饼干太小,换一块更大的
cookie += 1
return count
# 测试
g = [1, 2, 3] # 孩子的胃口
s = [1, 1] # 饼干尺寸
print(f"能满足 {find_content_children(g, s)} 个孩子") # 输出: 1
题目4:跳跃游戏(LeetCode 55)
问题:给定一个非负整数数组 nums,你最初位于数组的第一个下标。数组中的每个元素代表你在该位置可以跳跃的最大长度。判断你是否能够到达最后一个下标。
贪心策略 :维护能到达的最远位置
python
def can_jump(nums):
"""
跳跃游戏
返回:是否能到达最后一个位置
"""
max_reach = 0 # 当前能到达的最远位置
n = len(nums)
for i in range(n):
# 如果当前位置已经超过了能到达的最远位置
if i > max_reach:
return False
# 更新能到达的最远位置
max_reach = max(max_reach, i + nums[i])
# 如果已经能到达最后一个位置
if max_reach >= n - 1:
return True
return True
# 测试
nums1 = [2, 3, 1, 1, 4]
nums2 = [3, 2, 1, 0, 4]
print(can_jump(nums1)) # True
print(can_jump(nums2)) # False
题目5:跳跃游戏 II(LeetCode 45)
问题:跳到最后一个位置的最少跳跃次数。
贪心策略:在当前能到达的范围内,选择能跳到最远的下一步
python
def jump(nums):
"""
跳跃游戏 II - 最少跳跃次数
"""
n = len(nums)
if n <= 1:
return 0
jumps = 0 # 跳跃次数
current_end = 0 # 当前跳跃能到达的最远位置
max_reach = 0 # 下一次跳跃能到达的最远位置
for i in range(n - 1):
# 更新下一次能到达的最远位置
max_reach = max(max_reach, i + nums[i])
# 如果到达了当前跳跃的边界
if i == current_end:
jumps += 1
current_end = max_reach
# 如果已经能到终点
if current_end >= n - 1:
break
return jumps
# 测试
nums = [2, 3, 1, 1, 4]
print(f"最少需要 {jump(nums)} 次跳跃") # 输出: 2
题目6:划分字母区间(LeetCode 763)
问题:将字符串划分为尽可能多的片段,使得同一个字母最多出现在一个片段中。
贪心策略:记录每个字母最后出现的位置,在区间内扩展右边界
python
def partition_labels(s):
"""
划分字母区间
返回:每个片段的长度列表
"""
# 记录每个字符最后出现的位置
last_pos = {}
for i, char in enumerate(s):
last_pos[char] = i
result = []
start = 0
end = 0
for i, char in enumerate(s):
# 更新当前片段的结束位置
end = max(end, last_pos[char])
# 如果当前位置就是结束位置
if i == end:
result.append(end - start + 1)
start = end + 1
return result
# 测试
s = "ababcbacadefegdehijhklij"
print(f"分区长度: {partition_labels(s)}") # [9, 7, 8]
题目7:加油站(LeetCode 134)
问题:环形路上有 n 个加油站,第 i 个加油站有 gasi 升汽油,从 i 到 i+1 需要 costi 升汽油。从哪个加油站出发可以走完全程?
贪心策略:如果总油量 >= 总消耗,一定有解;从油量赤字之后的下一个位置开始
python
def can_complete_circuit(gas, cost):
"""
加油站问题
返回:起点索引,如果无法走完返回 -1
"""
total_gas = 0
total_cost = 0
current_gas = 0
start = 0
for i in range(len(gas)):
total_gas += gas[i]
total_cost += cost[i]
current_gas += gas[i] - cost[i]
# 如果当前油量不够,说明从 start 到 i 这段都不行
if current_gas < 0:
start = i + 1
current_gas = 0
# 如果总油量 >= 总消耗,一定有解
return start if total_gas >= total_cost else -1
# 测试
gas = [1, 2, 3, 4, 5]
cost = [3, 4, 5, 1, 2]
print(f"从第 {can_complete_circuit(gas, cost)} 个加油站出发")
三、贪心算法使用建议
✅ 什么时候用贪心?
-
问题具有贪心选择性质
-
问题的最优解包含子问题的最优解
-
经典问题:找零钱、活动选择、哈夫曼编码、最小生成树、最短路径(Dijkstra)
❌ 什么时候不用贪心?
-
需要回溯才能找到最优解(如背包问题的 0-1 版本)
-
当前选择会影响未来的选择(如旅行商问题)
-
贪心能得到局部最优但不能保证全局最优
四、常见题型总结
| 类型 | 经典题目 | 贪心策略 |
|---|---|---|
| 区间问题 | 活动选择、无重叠区间 | 按右端点排序 |
| 分配问题 | 分发饼干、分发糖果 | 排序后从最小开始匹配 |
| 跳跃问题 | 跳跃游戏 I/II | 维护能到达的最远位置 |
| 字符串处理 | 划分字母区间、移除数字 | 贪心选择边界 |
| 数组处理 | 加油站、股票买卖 | 累加差值找转折点 |
五、时间复杂度总结
-
排序类(分发饼干、活动选择):O(n log n)
-
遍历类(跳跃游戏、加油站):O(n)
-
空间复杂度:通常 O(1) 或 O(n)