题目描述
给定一个数组 prices
,它的第 i
个元素 prices[i]
表示一支给定股票第 i
天的价格。
你只能选择 某一天 买入这只股票,并选择在 未来的某一个不同的日子 卖出该股票。设计一个算法来计算你所能获取的最大利润。
返回你可以从这笔交易中获取的最大利润。如果你不能获取任何利润,返回 0
。
示例 1:
输入:[7,1,5,3,6,4] 输出:5 解释:在第 2 天(股票价格 = 1)的时候买入,在第 5 天(股票价格 = 6)的时候卖出,最大利润 = 6-1 = 5 。 注意利润不能是 7-1 = 6, 因为卖出价格需要大于买入价格;同时,你不能在买入前卖出股票。
示例 2:
输入:prices = [7,6,4,3,1] 输出:0 解释:在这种情况下, 没有交易完成, 所以最大利润为 0。
提示:
1 <= prices.length <= 10^5
0 <= prices[i] <= 10^4
详细问题分析
首先,让我们理解这个问题的关键约束:
- 我们只能买入和卖出一次
- 必须先买入再卖出(不能先卖出后买入)
- 买入和卖出必须在不同的日期
这个问题本质上是寻找数组中的两个元素,使得第二个元素减去第一个元素的差值最大,且第二个元素的索引必须大于第一个元素的索引。
解题思路分析
暴力解法
最直接的方法是使用双重循环,遍历所有可能的买入和卖出组合,找出利润最大的情况。
// 暴力解法 - 不推荐使用,仅用于理解问题
public int maxProfit(int[] prices) {
int maxProfit = 0;
for (int i = 0; i < prices.length; i++) {
for (int j = i + 1; j < prices.length; j++) {
int profit = prices[j] - prices[i];
if (profit > maxProfit) {
maxProfit = profit;
}
}
}
return maxProfit;
}
但这种方法的时间复杂度是O(n²),对于大规模数据会超时。
一次遍历法(最优解法)
更聪明的方法是利用一个关键洞察:要获得最大利润,我们需要以尽可能低的价格买入,然后在之后尽可能高的价格卖出。
我们可以在遍历数组的同时,记录到目前为止见到的最低价格,并计算如果在这个最低价格买入、当前价格卖出能获得的利润。这样,我们只需要遍历一次数组就能找到最大利润。
详细步骤如下:
- 初始化两个变量:minPrice 记录到目前为止的最低价格,maxProfit 记录最大利润
- 遍历价格数组:
- 如果当前价格低于 minPrice,更新 minPrice
- 否则,计算当前利润 = 当前价格 - minPrice,并更新 maxProfit 如果需要
算法执行过程
让我们使用示例 [7,1,5,3,6,4] 详细跟踪算法的执行过程:
初始状态:
- minPrice = 7 (第一个价格)
- maxProfit = 0
第2天:价格 = 1
- 1 < 7,更新 minPrice = 1
- maxProfit 仍为 0 (没有卖出机会)
第3天:价格 = 5
- 5 > 1,计算利润 = 5 - 1 = 4
- 4 > 0,更新 maxProfit = 4
第4天:价格 = 3
- 3 > 1,计算利润 = 3 - 1 = 2
- 2 < 4,maxProfit 不变,仍为 4
第5天:价格 = 6
- 6 > 1,计算利润 = 6 - 1 = 5
- 5 > 4,更新 maxProfit = 5
第6天:价格 = 4
- 4 > 1,计算利润 = 4 - 1 = 3
- 3 < 5,maxProfit 不变,仍为 5
最终结果:maxProfit = 5
示例图表
使用价格折线图和利润变化可以更直观地理解这个问题:
价格走势:
价格
^
7 | *
6 | *
5 | *
4 | *
3 | *
2 |
1 | *
0 +-----------------> 时间
1 2 3 4 5 6
对应的最低价格和最大利润变化:
Day 1: price = 7, minPrice = 7, maxProfit = 0
Day 2: price = 1, minPrice = 1, maxProfit = 0
Day 3: price = 5, minPrice = 1, maxProfit = 4
Day 4: price = 3, minPrice = 1, maxProfit = 4 (不变)
Day 5: price = 6, minPrice = 1, maxProfit = 5
Day 6: price = 4, minPrice = 1, maxProfit = 5 (不变)
边界情况分析
1. 空数组或只有一个元素的数组:
这种情况无法进行买卖交易,应返回0。
2. 价格持续下跌的情况:
例如 [7,6,4,3,1],最大利润为0,因为没有可以盈利的交易机会。
3. 价格持续上涨的情况:
例如 [1,2,3,4,5],最大利润为4,应该在第一天买入,最后一天卖出。
详细代码实现与分析
Java 实现
class Solution {
public int maxProfit(int[] prices) {
// 边界条件检查:如果数组为空或只有一个元素,无法完成交易,返回0
if (prices == null || prices.length <= 1) {
return 0;
}
// 初始化最低价格为第一天的价格
int minPrice = prices[0];
// 初始化最大利润为0(如果无法盈利就返回0)
int maxProfit = 0;
// 从第二天开始遍历价格数组
for (int i = 1; i < prices.length; i++) {
// 情况1:如果当前价格比历史最低价格还低,更新最低价格
if (prices[i] < minPrice) {
minPrice = prices[i];
}
// 情况2:如果当前价格高于历史最低价格,计算利润并更新最大利润
else {
int currentProfit = prices[i] - minPrice;
if (currentProfit > maxProfit) {
maxProfit = currentProfit;
}
}
// 调试信息(实际提交时可以移除)
// System.out.println("Day " + i + ": price = " + prices[i] +
// ", minPrice = " + minPrice + ", maxProfit = " + maxProfit);
}
return maxProfit;
}
}
代码中的核心思想是维护一个最低价格 minPrice 和一个最大利润 maxProfit。每次遇到比历史最低价格更低的价格时,就更新 minPrice;每当计算出的当前利润比历史最大利润更大时,就更新 maxProfit。
我们也可以用 Math.min() 和 Math.max() 方法简化代码:
class Solution {
public int maxProfit(int[] prices) {
// 边界条件检查
if (prices == null || prices.length <= 1) {
return 0;
}
// 初始化最低价格为一个非常大的值,这样第一个价格必定会小于它
int minPrice = Integer.MAX_VALUE;
// 初始化最大利润为0
int maxProfit = 0;
// 遍历价格数组
for (int price : prices) {
// 更新最低价格(使用Math.min简化代码)
minPrice = Math.min(minPrice, price);
// 更新最大利润(使用Math.max简化代码)
// 注意:即使当前价格小于最低价格,price - minPrice也会是负值
// 但由于maxProfit的初值为0,不会被负值更新
maxProfit = Math.max(maxProfit, price - minPrice);
}
return maxProfit;
}
}
C# 实现
public class Solution {
public int MaxProfit(int[] prices) {
// 边界条件检查:如果数组为空或只有一个元素,无法完成交易,返回0
if (prices == null || prices.Length <= 1) {
return 0;
}
// 初始化变量
int minPrice = prices[0]; // 历史最低价格
int maxProfit = 0; // 最大利润
// 详细跟踪每一天的情况
Console.WriteLine("初始状态: minPrice = " + minPrice + ", maxProfit = " + maxProfit);
// 从第二天开始遍历
for (int i = 1; i < prices.Length; i++) {
int currentPrice = prices[i];
// 检查是否需要更新最低价格
if (currentPrice < minPrice) {
minPrice = currentPrice;
Console.WriteLine("第 " + (i+1) + " 天: 价格 = " + currentPrice +
", 更新最低价格为 " + minPrice + ", maxProfit = " + maxProfit);
}
// 检查是否能获得更高的利润
else {
int currentProfit = currentPrice - minPrice;
if (currentProfit > maxProfit) {
maxProfit = currentProfit;
Console.WriteLine("第 " + (i+1) + " 天: 价格 = " + currentPrice +
", minPrice = " + minPrice + ", 更新最大利润为 " + maxProfit);
} else {
Console.WriteLine("第 " + (i+1) + " 天: 价格 = " + currentPrice +
", minPrice = " + minPrice + ", maxProfit = " + maxProfit);
}
}
}
return maxProfit;
}
}
更简洁的C#实现:
public class Solution {
public int MaxProfit(int[] prices) {
if (prices == null || prices.Length <= 1) {
return 0;
}
int minPrice = int.MaxValue;
int maxProfit = 0;
foreach (int price in prices) {
// 更新最低价格
minPrice = Math.Min(minPrice, price);
// 更新最大利润
maxProfit = Math.Max(maxProfit, price - minPrice);
}
return maxProfit;
}
}
复杂度分析
- 时间复杂度:O(n),其中 n 是价格数组的长度。我们只需要遍历一次数组。
- 空间复杂度:O(1),只使用了常数额外空间(minPrice 和 maxProfit 两个变量)。