1. LeetCode 122. 买卖股票的最佳时机 II
1.1 问题分析与解题思路
这道题的核心在于理解股票买卖的规则:可以多次买卖同一支股票,但必须在再次购买前出售掉之前的股票。这种设定下,我们的目标就是抓住所有的上涨区间。
想象一下股票价格走势图就像一座座小山丘。我们不需要预测最高点和最低点,只需要确保在每个上升坡道都参与其中。具体来说:
- 当今天的价格比昨天高时,我们就在昨天买入,今天卖出
- 如果连续多天上涨,这相当于每天都在买卖,效果等同于在最低点买入,最高点卖出
- 对于价格持平或下跌的日子,我们不做任何操作
这种贪心算法的思路之所以有效,是因为它完美捕捉了所有的盈利机会。每次有利润就立即锁定,不会错过任何上涨区间。
1.2 代码实现与优化
python复制class Solution:
def maxProfit(self, prices: List[int]) -> int:
profit = 0
for i in range(1, len(prices)):
if prices[i] > prices[i-1]:
profit += prices[i] - prices[i-1]
return profit
这个实现的时间复杂度是O(n),空间复杂度是O(1),已经是最优解。几个关键点需要注意:
- 从第二天开始比较(i从1开始)
- 只需要比较相邻两天的价格
- 不需要额外的存储空间
提示:在实际面试中,可能会被问到为什么这个贪心算法有效。可以解释为"所有上升区间的总和等于一系列连续小上升区间的累加"。
1.3 边界情况处理
- 空数组或单元素数组:直接返回0
- 持续下跌的市场:不会进行任何买卖,返回0
- 持续上涨的市场:累计所有单日涨幅
- 波动市场:只计算上涨日的差价
2. LeetCode 55. 跳跃游戏
2.1 问题理解与算法选择
这个问题要求判断是否能从数组起点跳到终点,其中每个元素表示在该位置可以跳跃的最大长度。这类问题通常可以用贪心算法解决。
关键观察点是:我们不需要关心具体跳到哪里,只需要关心最远能到达的位置。如果最远能到达的位置超过或等于最后一个下标,那么就成功了。
2.2 贪心算法实现
python复制class Solution:
def canJump(self, nums: List[int]) -> bool:
max_reach = 0
for i, num in enumerate(nums):
if i > max_reach:
return False
max_reach = max(max_reach, i + num)
if max_reach >= len(nums) - 1:
return True
return True
这个算法的时间复杂度是O(n),空间复杂度是O(1)。它的工作原理是:
- 维护一个max_reach变量,表示当前能到达的最远位置
- 遍历数组,如果当前位置超过了max_reach,说明无法到达
- 否则更新max_reach为当前位置能跳到的最远位置
- 如果max_reach超过或等于最后一个下标,返回True
2.3 实际应用中的注意事项
- 数组长度为1时直接返回True
- 遇到0时需要特别注意,只有当max_reach大于当前位置时才可能跳过
- 可以在发现max_reach足够大时提前终止循环
3. LeetCode 45. 跳跃游戏 II
3.1 问题升级与解决思路
这是跳跃游戏的进阶版,要求找到到达终点的最少跳跃次数。我们需要在贪心算法的基础上做一些调整。
核心思路是:在每一步都选择能让我们在下一步跳得最远的位置。这被称为"贪心选择性质"。
3.2 最优解的实现
python复制class Solution:
def jump(self, nums: List[int]) -> int:
jumps = 0
current_end = 0
farthest = 0
for i in range(len(nums) - 1):
farthest = max(farthest, i + nums[i])
if i == current_end:
jumps += 1
current_end = farthest
if current_end >= len(nums) - 1:
break
return jumps
这个算法同样是O(n)时间复杂度和O(1)空间复杂度。关键点在于:
- current_end表示当前跳跃能到达的最远位置
- farthest表示在当前跳跃范围内,下一步能到达的最远位置
- 当i到达current_end时,必须进行一次跳跃,并更新current_end为farthest
3.3 性能优化与边界情况
- 数组长度为1时直接返回0
- 可以在current_end超过终点时提前终止循环
- 确保不会出现无限循环(当nums[i]=0且i<current_end时)
4. LeetCode 1005. K次取反后最大化的数组和
4.1 问题分析与解决策略
这个问题要求我们通过最多K次取反操作,使得数组和最大化。正确的策略是:
- 优先取反最小的负数(即绝对值最大的负数)
- 如果没有负数剩余,且K还有剩余:
- 如果K是偶数,可以不改变数组(因为两次取反抵消)
- 如果K是奇数,取反最小的正数(即绝对值最小的数)
4.2 代码实现与解释
python复制class Solution:
def largestSumAfterKNegations(self, nums: List[int], k: int) -> int:
nums.sort()
i = 0
n = len(nums)
# 第一阶段:取反负数
while i < n and k > 0 and nums[i] < 0:
nums[i] = -nums[i]
k -= 1
i += 1
# 第二阶段:处理剩余的K次取反
if k > 0:
nums.sort() # 重新排序找到最小的数
if k % 2 == 1:
nums[0] = -nums[0]
return sum(nums)
这个算法的时间复杂度主要由排序决定,是O(n log n)。关键步骤:
- 先排序数组,方便找到最小的负数
- 尽可能多地取反负数
- 如果还有剩余次数,处理最小正数
4.3 实际应用中的技巧
- 第一次排序后,负数部分已经处理,只需要对剩余部分重新排序
- 可以使用最小堆来优化找最小数的过程
- 注意K可能远大于数组长度的情况
5. 算法学习的心得体会
在实际刷题过程中,我发现贪心算法虽然思路简单,但往往需要深入理解问题本质才能正确应用。对于这些跳跃游戏类问题,关键在于维护一个"当前能到达的最远位置"的变量,而不是纠结于具体的跳跃步骤。
对于股票买卖问题,理解价格变化的本质是关键——相邻日期的差价累加就等于总利润。这种分解问题的能力在解决其他算法问题时也非常有用。
最后,关于数组取反的问题,教会我们有时候需要分阶段处理问题:先处理明显的优化点(负数),再处理剩余的特殊情况。这种分而治之的思想在算法设计中非常普遍。