121. 买卖股票的最佳时机#
今天让我们一起深入探讨一个经典题目 - 买卖股票的最佳时机。这道题不仅在面试中常见,更重要的是它蕴含着一个重要的思维方式:如何在波动的数据中找到最优解。让我们用最直观的方式来理解这个问题。
📚 现实生活中的场景#
想象你是一个艺术品交易商。你有一件珍贵的艺术品,想要通过一次交易获得最大收益。你可以查看未来一段时间内这件艺术品的价格变化。问题是:在什么时候买入、什么时候卖出,才能获得最大收益呢?
问题描述#
题目目标#
给定一个数组 prices ,其中 prices[i] 表示某支股票第 i 天的价格。你只能选择某一天买入这只股票,并在未来某一天卖出,最多进行一次交易。请返回你能获得的最大利润;如果无法获得利润,返回 0。
示例 1#
输入: prices = [7,1,5,3,6,4]
输出: 5
说明: 把输入和输出放在一起看,会更容易直接抓住题目要求的变化。
💡 问题的本质#
用直白的话说,就是:给你一个数组,数组中的第i个元素表示某个商品在第i天的价格。你只能选择其中的某一天买入,然后在这天之后的某一天卖出。问题是:你能获得的最大利润是多少?
比如说:
输入价格:[7,1,5,3,6,4]
最大利润:5(在第2天买入,价格=1;在第5天卖出,价格=6)javaclass Solution {
public int maxProfit(int[] prices) {
// 边界条件:如果数组为空,或者天数不足 2 天,则无法完成“先买后卖”这一次交易
if (prices == null || prices.length <= 1) {
// 无法获利时,按题意返回 0
return 0;
}
// minPrice 表示“截至当前天之前(含当前天)看到的最低股价”,可作为最优买入候选
int minPrice = prices[0];
// maxProfit 表示“遍历到当前为止”能够得到的最大利润,初始为 0(不交易)
int maxProfit = 0;
// 从第 1 天(索引 1)开始遍历,因为第 0 天已用于初始化最低买入价
for (int i = 1; i < prices.length; i++) {
// 情况 1:当前价格更低,说明出现了更优买入点,更新最低价格
if (prices[i] < minPrice) {
// 把买入成本降到更低,为后续可能出现的更大利润做准备
minPrice = prices[i];
} else {
// 情况 2:当前价格不低于最低价,尝试把今天作为卖出日计算利润
// currentProfit = 今天卖出价 - 历史最低买入价
int currentProfit = prices[i] - minPrice;
// 用当前利润与历史最大利润比较,保留更大值
maxProfit = Math.max(maxProfit, currentProfit);
}
}
// 返回整个遍历过程中的最大利润
return maxProfit;
}
}
java📝 解题思路的演进#
让我们看看解决这个问题的思维是如何发展的:
1. 暴力解法(不推荐)#
最直观的想法是:尝试所有可能的买入和卖出组合。但这样的时间复杂度是O(n²),对于大规模数据来说非常低效。
2. 一次遍历解法(推荐)#
我们可以用一个更聪明的方式:在遍历过程中记录已经看到的最低价格,并尝试用当前价格减去这个最低价格来计算可能的利润。这样只需要遍历一次,时间复杂度是O(n)。
🎯 代码是如何工作的?#
让我们用具体的例子来理解算法的工作过程。 假设价格序列是:[7,1,5,3,6,4]
-
第一天(价格=7):
- minPrice = 7
- maxProfit = 0
-
第二天(价格=1):
- 发现新的最低价:minPrice = 1
- maxProfit 仍然是 0
-
第三天(价格=5):
- 当前利润 = 5 - 1 = 4
- maxProfit 更新为 4
-
第四天(价格=3):
- 当前利润 = 3 - 1 = 2
- maxProfit 保持 4
-
第五天(价格=6):
- 当前利润 = 6 - 1 = 5
- maxProfit 更新为 5
-
第六天(价格=4):
- 当前利润 = 4 - 1 = 3
- maxProfit 保持 5
🎨 让我们可视化这个过程#
<svg viewBox="0 0 600 400" xmlns="http://www.w3.org/2000/svg">
<!-- 背景 -->
<rect width="600" height="400" fill="#f8f9fa"/>
<!-- 坐标轴 -->
<line x1="50" y1="350" x2="550" y2="350" stroke="black" stroke-width="2"/>
<line x1="50" y1="50" x2="50" y2="350" stroke="black" stroke-width="2"/>
<!-- 价格曲线 -->
<path d="M 100 200 L 180 320 L 260 240 L 340 280 L 420 220 L 500 260"
fill="none" stroke="#2196f3" stroke-width="3"/>
<!-- 最佳买入点 -->
<circle cx="180" cy="320" r="6" fill="#4caf50"/>
<text x="160" y="340" fill="#4caf50">买入点</text>
<!-- 最佳卖出点 -->
<circle cx="420" cy="220" r="6" fill="#f44336"/>
<text x="400" y="200" fill="#f44336">卖出点</text>
<!-- 最大利润区间 -->
<path d="M 180 320 L 420 320 L 420 220 L 180 320"
fill="#2196f3" fill-opacity="0.1" stroke="#2196f3" stroke-dasharray="5,5"/>
<!-- 坐标轴标签 -->
<text x="300" y="380" text-anchor="middle">时间</text>
<text x="30" y="200" text-anchor="middle" transform="rotate(-90 30 200)">价格</text>
<!-- 说明文字 -->
<text x="300" y="100" text-anchor="middle" font-size="14">最大利润 = 卖出价 - 买入价</text>
</svg>
plaintext🌟 面试时的要点#
-
理解问题约束
- 只能进行一次交易(买入一次,卖出一次)
- 必须先买入后卖出
- 寻找的是最大利润,不是最高价格
-
解释算法思路
- 为什么不用暴力解法
- 如何通过记录最小值来优化
- 时间复杂度和空间复杂度的分析
-
考虑边界情况
- 空数组或只有一个元素的数组
- 单调递减的价格序列
- 价格都相同的情况
💡 相关的变种问题#
这个问题有几个常见的变种:
-
允许多次交易
java// 可以进行多次买卖,但不能同时进行多笔交易 public int maxProfitMultiple(int[] prices) { // profit:累计总利润,初始为 0 int profit = 0; // 从第 1 天开始遍历,和前一天比较价格变化 for (int i = 1; i < prices.length; i++) { // 如果今天比昨天贵,说明存在一段可获利的上升区间 if (prices[i] > prices[i-1]) { // 把这段“今天 - 昨天”的正收益累加到总利润中 profit += prices[i] - prices[i-1]; } } // 返回多次交易下的最大累计利润 return profit; } -
限制交易次数
java// 最多进行k次交易 public int maxProfitWithKTransactions(int[] prices, int k) { // 需要使用动态规划来解决 // 常见做法:定义“第 i 天、完成 t 次交易、是否持股”的状态并进行转移 // 这里只给出方法签名作为扩展,不展开具体实现 }
🎓 学习要点#
-
思维方式
- 如何将复杂问题简化
- 如何利用已知信息避免重复计算
-
代码优化
- 使用变量记录状态
- 一次遍历解决问题
-
实际应用
- 在实际交易中的应用
- 处理时间序列数据的思路
这道题看似简单,但它教会我们一个重要的思维方式:有时候,保持对历史最优值的追踪,能帮助我们更高效地解决问题。如果你对这个话题还有任何疑问,欢迎在评论区讨论!