一、问题
一名精明的投资者,面对每日波动的股票市场,如何才能找到最佳的买卖时机,实现利润最大化?
问题: 给定一个数组 prices,其中 prices[i] 表示某支股票在第 i 天的价格。你只能进行一次股票买卖交易(即先买入,后卖出,且必须在买入后的某一天卖出)。
设计一个算法,计算出可以获得的最大利润。如果无法获得任何利润(例如,股票价格持续下跌),则返回 0。
输入: 一个整数数组 prices,表示股票每日的价格。
输出: 能获得的最大利润。
约束条件:
- 必须先买入股票,才能卖出。
- 必须在买入之后的某一天卖出股票。
示例 1:
输入:prices = [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。
关键点:
- 一次交易: 只能买卖一次,简化了问题。
- 买卖时序: 必须先买后卖,限制了卖出时间必须在买入时间之后。
- 最大利润: 目标是找到一个买入点和一个卖出点,使得
卖出价格 - 买入价格的差值最大。 - 无利润情况: 如果找不到合适的买卖点,则返回 0。
二、问题分析
要解决股票买卖问题,核心在于找到合适的买入时间和卖出时间,使得收益最大化。 为了做到这一点,需要跟踪股票价格的变化,并记住到目前为止所看到的最低买入价格。
- 暴力解法: 可以尝试所有可能的买入和卖出组合,但时间复杂度较高。
- 更优解法: 是否可以使用动态规划或一次遍历的方法,在 O(n) 的时间内解决问题?
- 边界情况: 数组为空或只有一个元素时,应如何处理?
-
暴力解法 : 枚举所有可能的买入和卖出组合。对于每一天作为买入日期,遍历其后的所有日期作为卖出日期,计算利润并更新最大利润。 伪代码:
max_profit = 0 for i from 0 to n-1: // 买入日期 for j from i+1 to n: // 卖出日期 (必须在买入之后) profit = prices[j] - prices[i] max_profit = max(max_profit, profit) return max_profit -
动态规划 (Dynamic Programming) - 实际上并非最优解,但可以作为一种思考方向: 创建两个数组
min_prices[i](表示到第i天为止的最低股票价格) 和max_profit[i](表示到第i天为止的最大利润)。递推公式:min_prices[i] = min(min_prices[i-1], prices[i])max_profit[i] = max(max_profit[i-1], prices[i] - min_prices[i-1])
-
一次遍历 : 使用一个变量
min_price记录到目前为止的最低股票价格,使用一个变量max_profit记录到目前为止的最大利润。 在遍历数组的过程中,不断更新这两个变量。 -
贪心算法,实际上与一次遍历的思路一致: 贪心算法的核心思想是每一步都做出局部最优的选择,最终达到全局最优。 在这个问题中,每次都试图找到最低的买入价格,然后计算以当前价格卖出的利润,并不断更新最大利润。
复杂度对比:
| 解法 | 时间复杂度 | 空间复杂度 | 优点 | 缺点 |
|---|---|---|---|---|
| 暴力解法 | O(n^2) | O(1) | 简单易懂 | 时间复杂度高,容易超时 |
| 动态规划 | O(n) | O(n) | 提供另一种思考问题的角度 | 空间复杂度较高,并非最优解 |
| 一次遍历/贪心 | O(n) | O(1) | 时间和空间复杂度都是最优的 | 相对需要更深入的理解问题本质 |
一次遍历(或贪心算法)是解决股票买卖问题的最佳方法,因为它具有最优的时间复杂度和空间复杂度。 简单、高效,并且易于理解和实现。
三、算法实现
假设给定的数组为:[7, 1, 5, 3, 6, 4]
如果在图表上绘制给定数组中的数字,将会得到:

使用一个变量 min_price 记录到目前为止的最低股票价格,使用一个变量 max_profit 记录到目前为止的最大利润。 在遍历数组的过程中,不断更新这两个变量。
优点: 时间和空间复杂度都是最优的,是解决此问题的最佳方法。
代码实现:
class Solution {
public:
int maxProfit(vector<int>& prices) {
if (prices.size() < 2)
return 0;
int mins = prices[0];
int maxProfit = 0;
for (unsigned i = 1; i < prices.size(); ++i) {
mins = std::min(mins, prices[i]);
maxProfit = std::max(maxProfit, prices[i] - mins);
}
return maxProfit;
}
};
时间复杂度:O(n),只需要遍历一次。
空间复杂度:O(1),只使用了常数个变量。
四、问题变体 II
一位活跃的股票交易员,目标是通过多次买卖同一支股票来最大化利润。 每天你可以选择买入或卖出股票,但请记住,最多只能持有一股股票。 此外,也可以在同一天先买入股票,然后再卖出。
问题: 给定一个整数数组 prices,其中 prices[i] 表示某支股票在第 i 天的价格。 设计一个算法,计算出可以获得的最大利润。
输入: 一个整数数组 prices,表示股票每日的价格。
输出: 能获得的最大利润。
约束条件:
- 可以进行任意次数的买卖交易。
- 最多只能同时持有一股股票。
- 可以在同一天买入并卖出股票。
示例 1:
输入:prices = [7, 1, 5, 3, 6, 4]
输出:7
解释:
一种可能的策略是:
- 在第 2 天(股票价格 = 1)买入,在第 3 天(股票价格 = 5)卖出,利润 = 5 - 1 = 4。
- 在第 4 天(股票价格 = 3)买入,在第 5 天(股票价格 = 6)卖出,利润 = 6 - 3 = 3。
总利润为 4 + 3 = 7。
示例 2:
输入:prices = [1, 2, 3, 4, 5]
输出:4
解释:
一种可能的策略是:在第 1 天(股票价格 = 1)买入,在第 5 天(股票价格 = 5)卖出,利润 = 5 - 1 = 4。
总利润为 4。
示例 3:
输入:prices = [7, 6, 4, 3, 1]
输出:0
解释:在这种情况下,股票价格持续下跌,任何交易都无法获得正利润,因此最大利润为 0。
关键点:
- 多次交易: 允许进行任意次数的买卖。
- 最大持股数: 任何时候最多只能持有一股股票。
- 同一天交易: 可以在同一天买入并卖出。
- 最大利润: 目标是找到最佳的买卖策略,使得总利润最大。
- 无利润情况: 如果找不到合适的买卖点,则返回 0。
4.1、问题分析
与只能进行一次买卖不同,允许多次交易意味着可以在任何时候买入和卖出股票,只要遵循最多持有一股的规则。
贪心算法 (最优解):贪心算法的核心在于局部最优选择。 在此问题中,如果今天的价格比昨天的价格高,就在昨天买入,今天卖出,这样就能获得利润。将所有这些利润累加起来,得到的就是最大利润。
动态规划:虽然贪心算法是最佳选择,但也可以用动态规划来思考问题,帮助理解。
对比与选择:
| 特性 | 贪心算法 (Greedy) | 动态规划 (DP) |
|---|---|---|
| 时间复杂度 | O(n) | O(n) |
| 空间复杂度 | O(1) | O(n) (可优化至 O(1)) |
| 易于理解 | 容易 | 相对复杂 |
| 代码简洁 | 简洁 | 较长 |
| 适用性 | 股票多次买卖 | 更复杂的问题变种 |
对于这个股票多次买卖问题,贪心算法是最佳选择。 它简单、高效,并且保证能找到最优解。 动态规划虽然可行,但实现起来更复杂,而且空间复杂度更高。 因此,应该优先选择贪心算法。
4.2、贪心算法
本质上,贪心算法抓住了问题的一个关键性质:可以将一个大的交易周期分解为多个小的上升周期。 例如,如果价格是 [1, 2, 3, 4, 5],可以理解为在第一天买入,第五天卖出,也可以理解为在第一天买入第二天卖出,第二天买入第三天卖出…以此类推,最终利润是一样的。
算法步骤:
- 初始化
max_profit = 0。 - 遍历
prices数组,从第二天开始 (i = 1)。 - 对于每一天,比较
prices[i]和prices[i-1]。 - 如果
prices[i] > prices[i-1],说明存在上升趋势,将利润prices[i] - prices[i-1]加到max_profit中。 - 遍历结束后,
max_profit就是能获得的最大利润。
代码实现:
class Solution {
public:
int maxProfit(vector<int>& prices) {
int n = prices.size();
if (n < 2)
return 0;
int count = 0;
for (unsigned i = 1; i < prices.size(); ++i)
count += std::max(prices[i] - prices[i - 1], 0);
return count;
}
};
复杂度分析:
- 时间复杂度: O(n),只需要一次循环遍历数组。
- 空间复杂度: O(1),只需要常数级别的额外空间。
优势:
- 简单易懂:代码非常简洁,容易理解算法的逻辑。
- 高效:时间复杂度低,适合处理大规模数据。
- 最优解:贪心算法得到的结果就是最大利润。
4.3、动态规划
定义状态:dp[i][0] 表示在第 i 天不持有股票的最大利润,dp[i][1] 表示在第 i 天持有股票的最大利润。
状态转移:
dp[i][0] = max(dp[i-1][0], dp[i-1][1] + prices[i]):今天不持有股票,要么昨天就不持有,要么昨天持有今天卖出dp[i][1] = max(dp[i-1][1], dp[i-1][0] - prices[i]):今天持有股票,要么昨天就持有,要么昨天不持有今天买入
算法步骤:
- 初始化:
dp[0][0] = 0,dp[0][1] = -prices[0](第一天买入股票,利润为负)。 - 遍历
prices数组,从第二天开始 (i = 1)。 - 根据状态转移方程更新
dp[i][0]和dp[i][1]。 - 最终结果为
dp[n-1][0](最后一天不持有股票的最大利润)。
代码实现:
class Solution {
public:
int maxProfit(vector<int>& prices) {
vector<vector<int>>dp(prices.size(),vector<int>(2,0));
dp[0][0]=-prices[0];
dp[0][1]=0;
for(int i = 1;i<prices.size();i++){
//持有股票
dp[i][0]=max(dp[i-1][0],dp[i-1][1]-prices[i]);
//不持有
dp[i][1]=max(dp[i-1][1],dp[i-1][0]+prices[i]);
}
return max(dp[prices.size()-1][0],dp[prices.size()-1][1]);
}
};
复杂度分析:
- 时间复杂度: O(n),需要一次循环遍历数组。
- 空间复杂度: O(n),需要一个二维数组
dp来存储状态。 可以优化到 O(1) 的空间复杂度,因为每次只需要用到前一天的状态。
优势: 可以解决更复杂的问题变种,例如加入交易费等。
劣势: 代码更复杂,理解难度较高。而且空间复杂度较高(即使优化后仍然不如贪心算法)。
五、总结
探讨股票买卖问题,从单次交易到多次交易,循序渐进地分析了不同的解题策略。对于单次交易,一次遍历的贪心算法以其O(n)的时间复杂度和O(1)的空间复杂度脱颖而出,成为寻找最大利润的最优解。尽管动态规划提供了一种不同的思考角度,但在空间效率上略逊一筹。
文章分析了多次交易的股票买卖问题,重点介绍了贪心算法的巧妙应用,通过捕捉每日价格上涨的机会来最大化利润。与单次交易相同,贪心算法以其简洁性和效率,成为解决此类问题的首选方法。动态规划虽然仍可用,但在代码复杂性和空间复杂度上并不占优。理解问题的本质,选择合适的算法,是解决问题的关键。

6737

被折叠的 条评论
为什么被折叠?



