贪心算法(Greedy Algorithm)的核心思想是:在对问题求解时,每一步都选择当前看起来最好的选择,从而希望最终结果是全局最优的。
贪心算法不需要考虑整体最优,它只关心"眼前"的利益。对于某些特定问题,这种局部最优的选择确实能导致全局最优解。
1. LeetCode 121. 买卖股票的最佳时机 (简单)
题目大意:
给一个数组 prices,其中 prices[i] 表示第 i 天股票的价格。你只能选择某一天买入,并在未来的某一天卖出。求你能获得的最大利润。
思路:
要想赚钱,核心就是:在最低点买入,在最高点(且在买入之后)卖出。
- 遍历一遍数组。
- 贪心策略: 记录下到目前为止遇到的"最低价格"。
- 每一天都假设我今天卖出,利润 = 今天价格 - 之前的最低价格。
- 不断更新这个利润的最大值。
代码实现 (Python):
python
def maxProfit(prices):
min_price = float('inf') # 记录遇到的最低价格,初始化为无穷大
max_profit = 0 # 记录最大利润
for price in prices:
# 贪心:总是保留之前看到的最小值
if price < min_price:
min_price = price
# 贪心:如果今天卖出赚得更多,就更新最大利润
elif price - min_price > max_profit:
max_profit = price - min_price
return max_profit
2. LeetCode 55. 跳跃游戏 (中等)
题目大意:
给定一个非负整数数组 nums,你最初位于数组的第一个下标。数组中的每个元素代表你在该位置可以跳跃的最大长度。判断你是否能够到达最后一个下标。
思路:
不要去纠结"具体跳哪一步",而是关注"我最远能跳到哪"。
- 贪心策略: 维护一个变量
max_reach,表示当前能够到达的最远位置。 - 遍历数组,如果你当前所在的位置
i比max_reach还要远,说明根本走不到这一步,失败。 - 否则,尝试更新
max_reach = max(max_reach, i + nums[i])。 - 如果
max_reach已经超过或等于最后一个位置,成功。
代码实现 (Python):
python
def canJump(nums):
max_reach = 0
for i, jump in enumerate(nums):
# 如果当前位置已经超过了最远能到达的范围,说明到不了这
if i > max_reach:
return False
# 贪心:不断更新能跳到的最远边界
max_reach = max(max_reach, i + jump)
return True
3. LeetCode 45. 跳跃游戏 II (中等)
题目大意:
这题是上一题的进阶版。假设一定 能到达最后,求达到末尾所需的最小跳跃次数。
思路:
想象在跑步,每一步都有一个"最远射程"。
- 贪心策略: 不在乎每一小步跳哪,只在乎在当前这一跳的范围内,下一跳最远能跳到哪里。
- 维护三个变量:
jumps(跳了次数)、current_end(当前这一跳能到达的最远边界)、next_max_reach(下一跳最远能到的地方)。 - 遍历数组,当走到
current_end时,说明必须跳下一跳了,此时jumps += 1,并把边界更新为next_max_reach。
代码实现 (Python):
python
def jump(nums):
jumps = 0
current_end = 0 # 当前这步跳跃的最远边界
next_max_reach = 0 # 下一步最远能跳到的位置
# 注意:不需要遍历最后一个元素,因为到了倒数第二个如果能覆盖最后,就不用再跳了
for i in range(len(nums) - 1):
# 贪心:在当前范围内,寻找能跳得最远的落脚点
next_max_reach = max(next_max_reach, i + nums[i])
# 走到了当前这一跳的尽头
if i == current_end:
jumps += 1
current_end = next_max_reach # 开启下一跳的范围
return jumps
4. LeetCode 763. 划分字母区间 (中等)
题目大意:
给一个字符串 S。要把这个字符串划分为尽可能多的片段,同一字母最多出现在一个片段中。返回一个表示每个字符串片段长度的列表。
例如:S = "ababcbacadefegdehijhklij"
结果:[9, 7, 8](分别是 "ababcbaca", "defegde", "hijhklij")
思路:
关键点:同一个字母必须都在同一个片段里。 意味着如果一个片段里有字母 'a',那么这个片段至少要延伸到 'a' 最后一次出现的位置。
- 准备工作: 先遍历一遍字符串,记录每个字母最后一次出现的下标。
- 贪心策略: 再次遍历字符串,维护两个变量
start和end。end是当前片段中所有字母里,"最后出现位置"的最大值。- 当走到了
i == end的时候,说明当前片段里的所有字母,在后面都不会再出现了。 - 这就是一个完美的切割点!
代码实现 (Python):
python
def partitionLabels(s):
# 1. 统计每个字符最后出现的下标
last_occurrence = {char: i for i, char in enumerate(s)}
result = []
start = 0
end = 0
# 2. 遍历字符串
for i, char in enumerate(s):
# 贪心:当前片段的终点,至少要是当前字符最后出现的位置
end = max(end, last_occurrence[char])
# 如果走到了当前片段的终点
if i == end:
result.append(end - start + 1) # 记录长度
start = i + 1 # 更新下一个片段的起点
return result
总结:如何学贪心?
- 不要想复杂: 贪心通常不需要高深的数学证明。就想:"此时此刻,怎么做最划算?"
- 找"最"字: 题目中出现"最大利润"、"最少跳跃"、"最远距离",往往可以考虑贪心。
- 试探法: 如果发现可以通过局部最优选法直接得到答案(且举不出反例),那么贪心就是对的。
- 常见模版:
- 排序(很多贪心题需要先排序,虽然本页四题没用到,但很常见)。
- 维护一个
max_so_far(目前为止最大)。 - 双指针/滑动窗口辅助。