• 想要精通算法和SQL的成长之路 - 买卖股票的最佳时机系列


    前言

    想要精通算法和SQL的成长之路 - 系列导航

    一. 买卖股票的最佳时机(买卖一次)

    原题链接
    我们先来看下最简单的股票问题。给定一个数组 prices ,它的第 i 个元素 prices[i] 表示一支给定股票第 i 天的价格。

    你只能选择某一天买入这只股票,并选择在未来的某一个不同的日子卖出该股票。设计一个算法来计算你所能获取的最大利润。

    • 输入:[7,1,5,3,6,4]
    • 输出:5
    • 解释:在第 2 天(股票价格 = 1)的时候买入,在第 5 天(股票价格 = 6)的时候卖出,最大利润 = 6-1 = 5 。

    我们从题目所给信息可以得到;

    1. 股票只能买一次和卖一次。
    2. 那么当我们买入了一只股票,它的最大利润就是在历史最低点卖出。

    那么就可以用贪心的思想,在遍历股票价格的同时。我们遍历到一个价格,就假设当天把他卖出,同时维护历史最低价,然后计算差值就是最大利润。代码如下:

    public int maxProfit(int[] prices) {
        int minPrice = Integer.MAX_VALUE;
        int res = 0;
        for (int price : prices) {
            // 找到前几天的最小值
            minPrice = Math.min(minPrice, price);
            // 我们求得:如果卖当前股票,那么当前股票的 最大利润 = 当前价格 - 历史最小价
            res = Math.max(res, price - minPrice);
        }
        return res;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    二. 买卖股票的最佳时机II(买卖多次)

    原题链接
    在每一天,你可以决定是否购买和/或出售股票。你在任何时候最多只能持有 一 股票。你也可以先购买,然后在同一天出售。也就是在第一题的基础上,可以选择买卖多次。

    其实这样反而比第一题要简单:

    • 既然每天都可以买卖的话,那么我们只要保证,卖的时候一定是涨的。 价格下降的日子都不卖。
    • 也就是说,我们可以无视价格下降的日子

    代码如下:

    public int maxProfit(int[] prices) {
        int res = 0;
        for (int i = 0; i < prices.length - 1; i++) {
            int cost = prices[i + 1] - prices[i];
            // 只有涨的价格,才有利润。如果是亏的,不计入
            if (cost > 0) {
                res += cost;
            }
        }
        return res;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    三. 买卖股票的最佳时机III(最多两次)

    原题链接

    在第二题的基础上,改变了限制:股票最多只能买两次。

    这题目可就不能跟上面两道题一样了,我们需要开始用变量代表各个状态来完成价格的计算了。也就是动态规划。这里面用到的状态有三种:

    • 当前是第几天。
    • 当前是否持股:持股/为持股。
    • 卖出过几次:0次,1次,2次。

    那么我们用三维数组来表达:dp[i][j][r],代表当前天数的最大利润。

    • i:第几天。
    • j:可能的值:0,1。0代表未持股,1代表持股。
    • r:可能的值:0,1,2。卖出过几次。

    那么就有

    int len = prices.length;
    int[][][] dp = new int[len][2][3];
    // 第一天,未持股,没卖过
    dp[0][0][0] = 0;
    // 第一天,买入一只股票,没卖过。
    dp[0][1][0] = -prices[0];
    // 第一天不可能卖过,因此凡是r = 1或者2的全部是最小值。
    dp[0][0][1] = dp[0][0][2] = dp[0][1][1] = dp[0][1][2] = min;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    那么接下来需要开始分析各个状态情况下公式的推导:

    状态利润
    今日未持股,卖出过0次 dp[i][0][0]都没买过,利润自然是0,无论哪一天: dp[i][0][0] = 0
    今日未持股,卖出过1次 dp[i][0][1]dp[i][0][1] = Max(dp[i-1][1][0]+price[i](今日卖出),dp[i-1][0][1](昨日未持股))
    今日未持股,卖出过2次 dp[i][0][2]dp[i][0][2] = Max(dp[i-1][1][1]+price[i](今日卖出),dp[i-1][0][2](昨日未持股))
    持股,卖出过0次 dp[i][1][0]dp[i][1][0] = Max(dp[i-1][0][0] - price[i](昨日买入了),dp[i-1][1][0](前几天就买过了))
    持股,卖出过1次 dp[i][1][1]dp[i][1][1] = Max(dp[i-1][0][1] - price[i](昨日买入了),dp[i-1][1][1](前几天就买过了))
    持股,卖出过2次 dp[i][1][2]最多卖两次,不能再卖下去,这种情况不存在:dp[i][1][02 = Integer.MIN

    那么最后股票的最大利润就有两种情况:

    • 卖出过1次,未持股:dp[len-1][0][1]
    • 卖出过2次,未持股:dp[len-1][0][2]

    那么最终代码如下:

    public int maxProfit(int[] prices) {
        int[][][] dp = new int[prices.length][2][3];
        int min = Integer.MIN_VALUE / 2;
        // 第一天,未持股,没卖过
        dp[0][0][0] = 0;
        // 第一天,买入一只股票,没卖过。
        dp[0][1][0] = -prices[0];
        // 第一天不可能卖过,因此凡是r = 1或者2的全部是最小值。
        dp[0][0][1] = dp[0][0][2] = dp[0][1][1] = dp[0][1][2] = min;
        // 第一天相关的值已经初始化了,从第二天开始计算
        for (int i = 1; i < prices.length; i++) {
            // 套公式 ,当前未持股
            dp[i][0][0] = 0;
            dp[i][0][1] = Math.max(dp[i - 1][1][0] + prices[i], dp[i - 1][0][1]);
            dp[i][0][2] = Math.max(dp[i - 1][1][1] + prices[i], dp[i - 1][0][2]);
    		// 持股
            dp[i][1][0] = Math.max(dp[i - 1][0][0] - prices[i], dp[i - 1][1][0]);
            dp[i][1][1] = Math.max(dp[i - 1][0][1] - prices[i], dp[i - 1][1][1]);
            dp[i][1][2] = min;
        }
        return Math.max(0, Math.max(dp[prices.length - 1][0][1], dp[prices.length - 1][0][2]));
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    四. 买卖股票的最佳时机IV(最多k次)

    原题链接

    这题的难度其实并没有上升太多,因为在第三题里面,已经把状态的转移公式写的很清楚了。只不过是买卖的次数由2次变更为k次罢了。

    那么代码的不同无非就是两个:

    • 初始化dp数组的时候,2需要改成k+1
    • for循环中,需要在增加一个for循环。用来遍历k的下标数组。因为第三题中,k只有0,1两种可能,因此没写成for循环。

    其中的公式更改为下

    // 如果是持股         昨天买入(因此昨天未持股)  前几天就持股了
    dp[i][1][k] = max(dp[i-1][0][k] - price , dp[i-1][1][k])
    // 如果是未持股,      昨天卖出(因此昨天持股) 。 前几天也未持股
    dp[i][0][k] = max(dp[i-1][1][k-1] + price , dp[i-1][0][k])
    
    • 1
    • 2
    • 3
    • 4

    代码如下:

    // 第一天相关的值已经初始化了,从第二天开始计算
    for (int i = 1; i < prices.length; i++) {
        for (int k = 1; k <= K; k++) {
            // 如果是持股         昨天买入(因此昨天未持股)  前几天就持股了
            dp[i][1][k] = Math.max(dp[i - 1][0][k] - prices[i], dp[i - 1][1][k]);
            // 如果是未持股,      昨天卖出(因此昨天持股) 。 前几天也未持股
            dp[i][0][k] = Math.max(dp[i - 1][1][k - 1] + prices[i], dp[i - 1][0][k]);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    那么初始化相关的代码我们就要做(遍历r位):

    • 第一天,如果持股,那么无论k等于多少,都赋值为-prices[0]
    • 第一天,如果不持股,那么最大利润就是0。
    for (int k = 0; k <= K; k++) {
        dp[0][0][k] = 0;// 第一天不持股,k不重要
        dp[0][1][k] = -prices[0];// 第一天持股
    }
    
    • 1
    • 2
    • 3
    • 4

    如果是后面几天(遍历i位):

    • 没持股、没卖过,就等于没买过。最大利润为0。
    • 持股,没卖过。那么当前的值取 Max(昨天买入,前几天就持有)的最大值。
    for (int i = 1; i < prices.length; i++) {
        dp[i][0][0] = 0;
        dp[i][1][0] = Math.max(dp[i - 1][0][0] - prices[i], dp[i - 1][1][0]);
    }
    
    • 1
    • 2
    • 3
    • 4

    最终代码就是:

    public int maxProfit(int K, int[] prices) {
        int[][][] dp = new int[prices.length][2][K + 1];
        for (int k = 0; k <= K; k++) {
            dp[0][0][k] = 0;// 第一天不持股,k不重要
            dp[0][1][k] = -prices[0];// 第一天持股
        }
        // 第i天如果买入股票,没卖过,未持股
        for (int i = 1; i < prices.length; i++) {
            dp[i][0][0] = 0;
            dp[i][1][0] = Math.max(dp[i - 1][0][0] - prices[i], dp[i - 1][1][0]);
        }
        // 第一天相关的值已经初始化了,从第二天开始计算
        for (int i = 1; i < prices.length; i++) {
            for (int k = 1; k <= K; k++) {
                // 如果是持股         昨天买入(因此昨天未持股)  前几天就持股了
                dp[i][1][k] = Math.max(dp[i - 1][0][k] - prices[i], dp[i - 1][1][k]);
                // 如果是未持股,      昨天卖出(因此昨天持股) 。 前几天也未持股
                dp[i][0][k] = Math.max(dp[i - 1][1][k - 1] + prices[i], dp[i - 1][0][k]);
            }
        }
        return Math.max(0, Math.max(dp[prices.length - 1][0][K], dp[prices.length - 1][0][K - 1]));
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    五. 最佳买卖股票时机含冷冻期

    原题链接
    总结就是:

    • 可以多买多卖。
    • 卖了之后的那一天不能买,即1天冷冻期。

    如果有冷冻期,那么我们可以把股票的持有状态分为以下三种:我们用dp[i][j]来代表第i天的最大利润。j代表对应的状态。

    • 持有股票。dp[i][0]
    • 未持有股票,并且处于冷冻期dp[i][1]
    • 未持有股票,处于非冷冻期dp[i][2]

    先说第一种,持有股票的状态怎么来:

    • 第一种:昨日刚买的股票,此时处于非冷冻期(冷冻期不可买)
    • 第二种:前几天就持有了。
    dp[i][0] = Math.max(dp[i-1][2] - price[i] , dp[i-1][0]);
    
    • 1

    再说下未持有+冷冻期的状态怎么来,只有一种情况:

    • 就是昨天刚卖的。
    dp[i][1] = dp[i-1][0] + price[i]
    
    • 1

    未持有+ 非冷冻期的状态:

    • 第一种:昨天未持有+非冷冻期。
    • 第二种:昨天未持有+冷冻期。
    dp[i][2] = Math.max(dp[i-1][1] , dp[i-1][2]);
    
    • 1

    初始化即第一天买入:

    dp[0][0] = -price[0]
    
    • 1

    结果:

    public int maxProfit2(int[] prices) {
        int len = prices.length;
        // 0:持股。1:未持股+冷冻。2:未持股+非冷冻
        int[][] dp = new int[len][3];
        // 持股
        dp[0][0] = -prices[0];
        for (int i = 1; i < len; i++) {
            dp[i][0] = Math.max(dp[i - 1][2] - prices[i], dp[i - 1][0]);
            dp[i][1] = dp[i - 1][0] + prices[i];
            dp[i][2] = Math.max(dp[i - 1][1], dp[i - 1][2]);
        }
        // 最终肯定是未持股的状态,可能处于冷冻期,也可能处于非冷冻期
        return Math.max(dp[len - 1][1], dp[len - 1][2]);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    六. 买卖股票的最佳时机含手续费

    原题链接

    给定一个整数数组 prices,其中 prices[i]表示第 i 天的股票价格 ;整数 fee 代表了交易股票的手续费用。

    你可以无限次地完成交易,但是你每笔交易都需要付手续费。如果你已经购买了一个股票,在卖出它之前你就不能再继续购买股票了。返回获得利润的最大值。

    • 输入:prices = [1, 3, 2, 8, 4, 9], fee = 2
    • 输出:8
    • 解释:能够达到的最大利润:

    在此处买入 prices[0] = 1
    在此处卖出 prices[3] = 8
    在此处买入 prices[4] = 4
    在此处卖出 prices[5] = 9
    总利润: ((8 - 1) - 2) + ((9 - 4) - 2) = 8

    这个题目我们可以定义:

    • dp[i][0]:第i天未持有股票的最大利润。
    • dp[i][1]:第i天持有股票的最大利润。

    那么推导公式如下:

    // 今日不持有 = max (昨日卖出 ,和昨日不持有)
    dp[i][0] = Math.max(dp[i-1][1]+prices[i]-fee,dp[i-1][0]);
    // 今日持有 = max (昨日买入 ,和昨日持有)
    dp[i][1] = Math.max(dp[i-1][0]-prices[i],dp[i-1][1]);
    
    • 1
    • 2
    • 3
    • 4

    初始值:

    • 第一天不买入: dp[0][0] = 0
    • 第一天买入:dp[0][1] = -prices[0]

    最终结果:

    public int maxProfit3(int[] prices, int fee) {
        int len = prices.length;
        int[][] dp = new int[len][2];
        dp[0][0] = 0;
        dp[0][1] = -prices[0];
        for (int i = 1; i < len; i++) {
            // 今日不持有 = max (昨日卖出 ,和昨日不持有)
            dp[i][0] = Math.max(dp[i - 1][1] + prices[i] - fee, dp[i - 1][0]);
            // 今日持有 = max (昨日买入 ,和昨日持有)
            dp[i][1] = Math.max(dp[i - 1][0] - prices[i], dp[i - 1][1]);
        }
        // 返回最终结果,一定是不持有状态
        return dp[len - 1][0];
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    七. 总结

    • 买卖股票(一次买卖):贪心算法。我们假设股票在卖出去的时候,永远都是历史最小价格收入的。
    • 买卖股票(多次买卖):一次遍历。只看涨的,不看降的。
    • 买卖股票(最多两次):三维数组,dp[i][j][r] :第i天,j代表是否持有:0,1。r代表卖过几次:0,1,2。
    • 买卖股票(最多k次):同样三维数组dp[i][j][r] :第i天,j代表是否持有:0,1。r代表卖过几次:0,1,2…k。
    • 买卖股票(含冷冻期):二维数组dp[i][j],第i天,j用三个数字代表三个状态。持有、未持有+冷冻期、未持有+非冷冻期
    • 买卖股票(含手续费):二维数组dp[i][j],第i天,j用两个数字代表两个状态:持有、未持有。
  • 相关阅读:
    metinfo 6.0.0 任意文件读取漏洞复现
    反碎片化技术(外部碎片)的原理
    Security at Datalink, network and Transport
    centos7服务器安全策略
    Linux系统通过console口连接交换机
    防止血糖飙升,你需要知道这12个技巧
    springboot毕设项目车辆违章信息管理系统72bl2(java+VUE+Mybatis+Maven+Mysql)
    人工智能AI风口已开:如何赋予UI设计与视频剪辑新生命
    是谁还没听过杨氏矩阵~原理和实现代码都已经准备好了
    测试项目中的风险管理
  • 原文地址:https://blog.csdn.net/Zong_0915/article/details/127651419