【算法】面试官:股票交易算法怎么写?我用这招征服了他! (附C++代码)

一、问题

一名精明的投资者,面对每日波动的股票市场,如何才能找到最佳的买卖时机,实现利润最大化?

问题: 给定一个数组 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) 的时间内解决问题?
  • 边界情况: 数组为空或只有一个元素时,应如何处理?
  1. 暴力解法 : 枚举所有可能的买入和卖出组合。对于每一天作为买入日期,遍历其后的所有日期作为卖出日期,计算利润并更新最大利润。 伪代码:

    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
    
  2. 动态规划 (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])
  3. 一次遍历 : 使用一个变量 min_price 记录到目前为止的最低股票价格,使用一个变量 max_profit 记录到目前为止的最大利润。 在遍历数组的过程中,不断更新这两个变量。

  4. 贪心算法,实际上与一次遍历的思路一致: 贪心算法的核心思想是每一步都做出局部最优的选择,最终达到全局最优。 在这个问题中,每次都试图找到最低的买入价格,然后计算以当前价格卖出的利润,并不断更新最大利润。

复杂度对比:

解法时间复杂度空间复杂度优点缺点
暴力解法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
解释:
一种可能的策略是:

  1. 在第 2 天(股票价格 = 1)买入,在第 3 天(股票价格 = 5)卖出,利润 = 5 - 1 = 4。
  2. 在第 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],可以理解为在第一天买入,第五天卖出,也可以理解为在第一天买入第二天卖出,第二天买入第三天卖出…以此类推,最终利润是一样的。

算法步骤:

  1. 初始化 max_profit = 0
  2. 遍历 prices 数组,从第二天开始 (i = 1)。
  3. 对于每一天,比较 prices[i]prices[i-1]
  4. 如果 prices[i] > prices[i-1],说明存在上升趋势,将利润 prices[i] - prices[i-1] 加到 max_profit 中。
  5. 遍历结束后,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]) :今天持有股票,要么昨天就持有,要么昨天不持有今天买入

算法步骤:

  1. 初始化:dp[0][0] = 0dp[0][1] = -prices[0] (第一天买入股票,利润为负)。
  2. 遍历 prices 数组,从第二天开始 (i = 1)。
  3. 根据状态转移方程更新 dp[i][0]dp[i][1]
  4. 最终结果为 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)的空间复杂度脱颖而出,成为寻找最大利润的最优解。尽管动态规划提供了一种不同的思考角度,但在空间效率上略逊一筹。

文章分析了多次交易的股票买卖问题,重点介绍了贪心算法的巧妙应用,通过捕捉每日价格上涨的机会来最大化利润。与单次交易相同,贪心算法以其简洁性和效率,成为解决此类问题的首选方法。动态规划虽然仍可用,但在代码复杂性和空间复杂度上并不占优。理解问题的本质,选择合适的算法,是解决问题的关键。

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Lion 莱恩呀

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值