贪心算法基础原理与题目说明
文章目录
- 贪心算法基础原理与题目说明
-
- [一、 什么是贪心算法?](#一、 什么是贪心算法?)
-
- [1.1 贪心算法的显著特点](#1.1 贪心算法的显著特点)
- [二、 股票买卖模型](#二、 股票买卖模型)
-
- [[121. 买卖股票的最佳时机](https://leetcode.cn/problems/best-time-to-buy-and-sell-stock/)](#121. 买卖股票的最佳时机)
- [三、 跳跃游戏系列(区间覆盖问题)](#三、 跳跃游戏系列(区间覆盖问题))
-
- [[55. 跳跃游戏](https://leetcode.cn/problems/jump-game/)](#55. 跳跃游戏)
- [[45. 跳跃游戏 II](https://leetcode.cn/problems/jump-game-ii/)](#45. 跳跃游戏 II)
- [四、 贪心与哈希的巧妙结合](#四、 贪心与哈希的巧妙结合)
-
- [[763. 划分字母区间](https://leetcode.cn/problems/partition-labels/)](#763. 划分字母区间)
🔗 查看完整专栏(LeetCode基础算法专栏)
特别说明:
本文为个人的 LeetCode 刷题与学习笔记,内容仅供学习与交流使用,禁止转载或用于商业用途。需要强调的是,文中的题目解法不一定是最优解(可能存在时间或空间复杂度的进一步优化空间),主要目的是分享个人的解题思路与逻辑实现,仅供参考。 笔记内容为个人理解与总结,可能存在疏漏或偏差,欢迎读者自行甄别并交流探讨。

一、 什么是贪心算法?
贪心算法(Greedy Algorithm)的核心思想是:局部最优 → \rightarrow → 全局最优。
在对问题求解时,总是做出在当前看来是最好的选择。也就是说,它不从整体最优上加以考虑,所做出的仅是在某种意义上的局部最优解,并期望通过这一系列的局部最优选择,最终推导出全局最优解。
1.1 贪心算法的显著特点
- 无后效性(不回溯):每一步的选择一旦做出,就不可更改,绝不回头重新考虑其他的可能性。
- 注重当下:不考虑未来的长远影响,只根据当前状态做出最有利的判断。
- 代码极简,证明极难 :贪心算法的代码往往非常短小、顺畅、直接;真正的难点在于证明该问题是否满足"贪心选择性质"(即证明局部最优真的能组合成全局最优)。
二、 股票买卖模型
121. 买卖股票的最佳时机
题目描述:
给定一个数组 prices ,它的第 i 个元素 prices[i] 表示一支给定股票第 i 天的价格。
你只能选择某一天 买入这只股票,并选择在未来的某一个不同的日子卖出该股票。设计一个算法来计算你所能获取的最大利润。若不能获取任何利润,返回 0。
解题思路:
卖出价固定时,买入价越低 → \rightarrow → 利润越高。
贪心策略:将遍历到的每一天都当作"潜在的卖出日"。对于这个卖出日,我们只关心它之前出现过的历史最低价格,其他较高的价格波动对我们毫无意义。
-
遍历每一天。
-
实时维护并更新到今天为止的历史最低价(最优买入价)。
-
计算如果今天卖出的利润(今天价格 - 历史最低价),并全程更新全局最大利润。
(这就是贪心的本质:每一步只保留对我最有利的局部最优信息,扔掉所有无用信息)
核心代码:
py
from typing import List
class Solution:
def maxProfit(self, prices: List[int]) -> int:
"""
贪心策略:把每一天作为潜在卖出日,只用这一日之前的最低买入价作为成本来计算利润,持续更新利润最大值。
"""
min_cost = float('inf')
max_profit = 0
# 遍历潜在卖出日
for price in prices:
# 维护历史最低买入价
min_cost = min(price, min_cost)
# 计算当前利润,并更新全局最大利润
max_profit = max(max_profit, price - min_cost)
return max_profit
三、 跳跃游戏系列(区间覆盖问题)
跳跃游戏系列是贪心算法在"区间覆盖"问题上的经典体现。核心在于维护当前可达的最远边界。
55. 跳跃游戏
题目描述:
给你一个非负整数数组 nums ,你最初位于数组的第一个下标。数组中的每个元素代表你在该位置可以跳跃的最大长度。
判断你是否能够到达最后一个下标,如果可以,返回 true ;否则,返回 false 。
解题思路:
核心贪心策略:始终维护当前可到达的最远位置(最远覆盖范围)。通过局部最优(每一步更新最远可达范围)推导全局最优(能否覆盖数组终点)。
- 遍历数组,只要当前位置
i在可到达的范围max_arrive内,就尝试用i + nums[i]去更新max_arrive。 - 只要
max_arrive大于等于数组最后一个下标,说明终点可达,提前返回True。 - 这种贪心选择能确保不遗漏任何可到达终点的可能性。
核心代码:
py
from typing import List
class Solution:
def canJump(self, nums: List[int]) -> bool:
"""
贪心策略:每次更新最远可以到达的位置,判断最远可达位置是否覆盖了数组终点。
"""
max_arrive = 0
n = len(nums)
for i in range(n):
# 1. 只有当前位置可达时,才能从这里继续向后跳
if i <= max_arrive:
# 2. 更新最远可达位置
max_arrive = max(max_arrive, i + nums[i])
# 3. 已经覆盖终点,提前返回
if max_arrive >= n - 1:
return True
return False
45. 跳跃游戏 II
题目描述:
给定一个长度为 n 的 0 索引整数数组 nums。初始位置在下标 0。每个元素 nums[i] 表示从索引 i 向后跳转的最大长度。
返回到达 n - 1 的最小跳跃次数 。测试用例保证一定可以到达 n - 1。
解题思路:
本题是跳跃游戏的进阶,求最小跳跃次数。
核心贪心策略:在当前这步能覆盖的所有起跳点中,选择那个能把"下一步的最远边界"推得更远的点。
- 维护两个变量:
max_arrive(当前探路能找到的全局最远位置)和cur_arrive(当前这一步跳跃所能覆盖的右边界)。 - 遍历数组(不包含最后一个元素),不断更新
max_arrive。 - 当遍历索引
i到达了当前步的边界cur_arrive时,说明当前这步的潜力已经用尽,必须 进行下一次跳跃了。此时跳跃次数+1,并将边界更新为max_arrive。
核心代码:
py
from typing import List
class Solution:
def jump(self, nums: List[int]) -> int:
"""
局部最优:单次跳跃覆盖最大距离 → 全局最优:总跳跃次数最少
"""
max_arrive = 0 # 探路的最远边界
cur_arrive = 0 # 当前跳跃步数能覆盖的右边界
ans = 0 # 跳跃次数
# 不遍历最后一个元素,因为题目保证能到达,若正好在终点起跳会多算一次
for i in range(len(nums) - 1):
# 探索在当前步的范围内,下一步最远能跳到哪
max_arrive = max(max_arrive, i + nums[i])
# 当遍历到当前步的极限边界时,必须进行下一次跳跃
if i == cur_arrive:
ans += 1
cur_arrive = max_arrive
return ans
四、 贪心与哈希的巧妙结合
763. 划分字母区间
题目描述:
给你一个字符串 s 。我们要把这个字符串划分为尽可能多的片段,同一字母最多出现在一个片段中。返回一个表示每个字符串片段的长度的列表。
解题思路:
贪心策略:寻找每个片段可能的最小结束下标。
由于同一个字母必须在一个片段中,这意味着如果当前片段包含了字符 c,那么这个片段的结束位置至少要延伸到 c 在字符串中最后一次出现的位置。
- 第一次遍历:用哈希表(或字典)记录每个字符最后出现的位置索引。
- 第二次遍历:维护当前片段的起始点
start和不断动态延伸的结束边界end。 - 每遍历到一个字符,就用它的最后出现位置去挑战并更新
end。 - 当遍历索引
i刚好到达end时,说明当前片段内所有字符的最远位置都只到这,可以完美切割。记录长度,进入下一个片段。
核心代码:
py
import collections
from typing import List
class Solution:
def partitionLabels(self, s: str) -> List[int]:
"""
贪心策略:维护当前区间内所有字符的最远出现位置,当遍历到达这个最远位置时,完成一次最小合法的完美切割。
"""
# 1. 记录每个字符最后一次出现的位置
endhash = collections.defaultdict(int)
for i in range(len(s)):
endhash[s[i]] = i
start = 0
end = 0
ans = []
# 2. 遍历字符串,寻找切分点
for i in range(len(s)):
# 动态延伸当前片段的结束边界
end = max(end, endhash[s[i]])
# 当索引到达当前要求的最远边界时,完成一次合法切分
if i == end:
ans.append(end - start + 1)
# 更新下一个片段的起始点
start = end + 1
return ans