【力扣一刷】代码随想录day49(动态规划part10-12 - 买卖股票专题:121 I、122 II、123 III、188 IV、309 含冷冻期、714 含手续费、买卖股票专题总结)

目录

【121. 买卖股票的最佳时机】简单题

方法一  暴力(超时)

方法二  贪心(简单)

方法三  动态规划(二维数组)

方法四  动态规划(两个变量滚动 / 一维数组滚动)

【122. 买卖股票的最佳时机 II】中等题

方法一  贪心(很好理解,简单)

方法二  动态规划(二维数组)

方法三  动态规划(两个变量滚动 / 一维数组滚动)

【123. 买卖股票的最佳时机 III】困难题

方法一  动态规划 (二维数组)

方法二  动态规划(一维滚动数组)

【188. 买卖股票的最佳时机 IV】困难题

【309. 买卖股票的最佳时机含冷冻期】中等题

方法一  动态规划(二维数组)

方法二  动态规划(一维滚动数组)

【714. 买卖股票的最佳时机含手续费】中等题

【买卖股票专题总结】


121. 买卖股票的最佳时机】简单题

方法一  暴力(超时)

class Solution {
    public int maxProfit(int[] prices) {
        int res = 0;
        for (int i = 0; i < prices.length; i++){
            for (int j = i + 1; j < prices.length; j++){
                res = Math.max(res, prices[j] - prices[i]);
            }
        }
        return res;
    }
}
  • 时间复杂度:O(n²),for循环嵌套遍历
  • 空间复杂度:O(1)

方法二  贪心(简单)

思路:动态更新当天左边区间的最小值,获取当天的最大利润,更新全局最大利润。

class Solution {
    public int maxProfit(int[] prices) {
        int res = 0;
        int min = Integer.MAX_VALUE;
        for (int i = 0; i < prices.length; i++){
            min = Math.min(min, prices[i]);
            res = Math.max(res, prices[i] - min);
        }
        return res;
    }
}
  • 时间复杂度:O(n),for循环遍历一次
  • 空间复杂度:O(1)

方法三  动态规划(二维数组)

步骤:

1、确定 dp[i] 的含义

对于第 i 天,有两种状态,持有和不持有,那么就需要二维数组存储每天的两个状态。

那么,可以假设:

dp[i][0] 对应第 i 天持有的状态,表示第 i 天持有股票时手上的最高剩余金额。

dp[i][1] 对应第 i 天不持有的状态,表示第 i 天不持有股票时手上的最高剩余金额。

最后,取最后一天不持有状态对应的最高剩余金额就是最大利润。

2、确定递推关系(重要前提:只能对这只股票买卖一次,默认初始金额为0)

dp[i][0]:已经持有,证明要么当天之前买入,要么刚好当天买入

  • 如果当天之前就已买入,即当天不用花钱再买,那么dp[i][0] = dp[i - 1][0]
  • 如果当天之前没有买入,那么当天就需要购买,那么dp[i][0] = 0 - prices[i] = - prices[i]

dp[i][1]:不持有,证明要么没买,要么当天之前卖出,要么刚好当天卖出

  • 如果到当天为止都没买入,则剩余金额依然为0,dp[i][1] = 0(后面两个状态>=0,这个状态可以忽略)
  • 如果当天之前就已卖出,即当天不用再卖,那么dp[i][1] = dp[i - 1][1]
  • 如果当天之前没有卖出,那么需要昨天持有的情况下,当天才能卖出,那么dp[i][1] = dp[i-1][0]  + prices[i]

3、确定初始值

由递推关系可以看出,当天的数据基本都是由昨天的数据推断而来,所以需要先确定第0天两个状态的取值:

  • dp[0][0]:第0天是持有状态,只能是第0天买入,所以 dp[0][0] = - prices[0].
  • dp[0][1]:第0天是不持有状态,则肯定只能是当天没买,所以 dp[0][1] = 0.
class Solution {
    public int maxProfit(int[] prices) {
        int[][] dp = new int[prices.length][2];
        dp[0][0] = -prices[0]; // 第0天买入才能持有
        dp[0][1] = 0; // 第0天没买,不持有,0元

        for (int i = 1; i < prices.length; i++){
            // 获取当天持有状态下的最高剩余金额
            dp[i][0] = Math.max(dp[i-1][0], -prices[i]);

            // 获取当天不持有状态下的最高剩余金额
            dp[i][1] = Math.max(dp[i-1][1], dp[i-1][0] + prices[i]);
        } 
        return dp[prices.length - 1][1]; // 最后一天肯定不持有的状态下是最大利润
    }
}
  • 时间复杂度:O(n),for循环遍历一次
  • 空间复杂度:O(n),额外的二维数组

方法四  动态规划(两个变量滚动 / 一维数组滚动)

思路:模仿背包问题进行滚动,降低空间复杂度,但是要注意两个状态的更新顺序

  • 当天不持有状态的最高剩余金额与昨天的两个状态都有关先更新,覆盖昨天的不持有状态对今天的持有状态不影响
  • 当天持有状态下的最高剩余金额只与昨天的持有状态有关后更新,否则覆盖昨天的持有状态
class Solution {
    public int maxProfit(int[] prices) {
        int keep = -prices[0];
        int notKeep = 0;

        for (int i = 1; i < prices.length; i++){
            // 获取当天不持有状态下的最高剩余金额(与昨天的两个状态都有关,先更新,覆盖昨天的不持有状态对今天的持有状态不影响)
            notKeep = Math.max(notKeep, keep + prices[i]);
            // 获取当天持有状态下的最高剩余金额(只与昨天的持有状态有关,后更新,否则覆盖昨天的持有状态)
            keep = Math.max(keep, -prices[i]);
        } 
        return notKeep; // 最后一天肯定不持有的状态下是最大利润
    }
}
  • 时间复杂度:O(n),for循环遍历一次
  • 空间复杂度:O(1),只用了两个变量存储


122. 买卖股票的最佳时机 II】中等题

方法一  贪心(很好理解,简单)

思路:只要当天比昨天的股价高,那么就能赚差价,叠加所有差价就是最大利润

class Solution {
    public int maxProfit(int[] prices) {
        int res = 0;
        for (int i = 1; i < prices.length; i++) res += Math.max(prices[i] - prices[i-1], 0);
        return res;
    }
}
  • 时间复杂度:O(n),for循环遍历一次
  • 空间复杂度:O(1)

方法二  动态规划(二维数组)

思路:与121题一致,区别在于当天买入的时候,需要在昨天不持有的状态下购买。

dp[i][0] = dp[i-1][1] - prices[i]

class Solution {
    public int maxProfit(int[] prices) {
        int[][] dp = new int[prices.length][2];
        dp[0][0] = - prices[0];
        dp[0][1] = 0;

        for (int i = 1; i < prices.length; i++){
            // 获取当天持有状态下的最高剩余金额
            dp[i][0] = Math.max(dp[i-1][0], dp[i-1][1] - prices[i]); // 当天买的前提是昨天不持有

            // 获取当天不持有状态下的最高剩余金额
            dp[i][1] = Math.max(dp[i-1][1], dp[i-1][0] + prices[i]); // 当天卖的前提是昨天持有
        }
        return dp[prices.length - 1][1];
    }
}
  • 时间复杂度:O(n),for循环遍历一次
  • 空间复杂度:O(n),额外的二维数组

方法三  动态规划(两个变量滚动 / 一维数组滚动)

思路:与121题思路基本一致,但是区别在于当天持有与不持有的最高剩余金额 都与昨天的两个状态有关,所以在滚动时, 需要防止当天的值覆盖昨天的值利用额外的变量记录先改变的状态即可。
class Solution {
    public int maxProfit(int[] prices) {
        int keep = -prices[0];
        int notKeep = 0;

        for (int i = 1; i < prices.length; i++){
            // 先更新的是当天持有的状态,把昨天持有的状态先记录下来(用于更新当天的不持有状态)
            int keepRaw = keep; 
            keep = Math.max(keep, notKeep - prices[i]);
            notKeep = Math.max(notKeep, keepRaw + prices[i]);
        }
        return notKeep;
    }
}
  • 时间复杂度:O(n),for循环遍历一次
  • 空间复杂度:O(1)

123. 买卖股票的最佳时机 III】困难题

方法一  动态规划 (二维数组)

思路:最多可以交易两次,而且不能同时进行多次交易,即不能同时持有两只股,在再次购买前出售掉之前买入的股票,所以一共4个状态,需要用二维数组记录状态值。

1、状态0:第一次(持有)

  • 当天之前就持有,保持昨天的持有状态,即dp[i][0] = dp[i-1][0]
  • 当天才进行第一次购买,购买前剩余金额肯定为0,即dp[i][0] = 0 - prices[i] = - prices[i]

2、状态1:第一次(不持有)

  • 之前都没买过,dp[i][1] = 0(后面两个状态>=0,这个状态可以忽略)
  • 当天之前就售出,保持昨天的不持有状态,dp[i][1] = dp[i-1][1]
  • 当天才进行第一次售出,昨天已经处于第一次持有状态 dp[i][1] = dp[i-1][0] + prices[i]

3、状态2:第二次(持有)

  • 当天之前就持有,保持昨天的持有状态,dp[i][2] = dp[i-1][2]
  • 当天才进行第二次购买,昨天已经处于第一次不持有状态,dp[i][2] = dp[i-1][1] - prices[i]

4、状态3:第二次(不持有)

  • 当天之前就售出,保持昨天的不持有状态,dp[i][3] = dp[i-1][3]
  • 当天才进行第二次售出,昨天已经处于第二次持有状态,dp[i][3] = dp[i-1][2] + prices[i]
class Solution {
    public int maxProfit(int[] prices) {
        // 4种状态:第一次(持有)、第一次(不持有)、第二次(持有)、第二次(不持有)
        int dp[][] = new int[prices.length][4];
        // 初始值
        dp[0][0] = -prices[0]; // 第一次购买
        dp[0][1] = 0;
        dp[0][2] = -prices[0]; // 第二次购买
        dp[0][3] = 0;

        for (int i = 1; i < prices.length; i++){
            // 第一次(持有)
            dp[i][0] = Math.max(dp[i-1][0], 0 - prices[i]); // 注意当天进行第一次购买前,剩余金额肯定为0
            // 第一次(不持有)
            dp[i][1] = Math.max(dp[i-1][1], dp[i-1][0] + prices[i]); // 当天进行第一次售出,昨天肯定已经处于第一次持有状态
            // 第二次(持有)
            dp[i][2] = Math.max(dp[i-1][2], dp[i-1][1] - prices[i]); // 当天进行第二次购买,昨天肯定已经处于第一次不持有状态
            // 第二次(不持有)
            dp[i][3] = Math.max(dp[i-1][3], dp[i-1][2] + prices[i]); // 当天进行第二次售出,昨天肯定已经处于第二次持有状态
        }
        return dp[prices.length-1][3];
    }
}
  • 时间复杂度:O(n),for循环遍历一次
  • 空间复杂度:O(n),二维数组第二个维度固定,第一个维度与输入数组的长度有关

方法二  动态规划(一维滚动数组)

思路:当天的状态3与昨天的状态2有关,当天的状态2与昨天的状态1有关,当天的状态1与昨天的状态0有关,当天的状态0也与昨天的状态0有关。所以需要倒序更新当天的状态,防止当天状态覆盖昨天的状态。

class Solution {
    public int maxProfit(int[] prices) {
        // 4种状态:第一次(持有)、第一次(不持有)、第二次(持有)、第二次(不持有)
        int dp[] = new int[4];
        // 初始值
        dp[0] = -prices[0]; // 第一次购买
        dp[1] = 0;
        dp[2] = -prices[0]; // 第二次购买
        dp[3] = 0;

        for (int i = 1; i < prices.length; i++){
            dp[3] = Math.max(dp[3], dp[2] + prices[i]);
            dp[2] = Math.max(dp[2], dp[1] - prices[i]);
            dp[1] = Math.max(dp[1], dp[0] + prices[i]);
            dp[0] = Math.max(dp[0], 0 - prices[i]); 
        }
        return dp[3];
    }
}
  • 时间复杂度:O(n),for循环遍历一次
  • 空间复杂度:O(1),一维数组长度固定为4


188. 买卖股票的最佳时机 IV】困难题

思路:参考123题的方法二,本题与132题的区别在于,最多交易的次数不固定,由输入参数k指定,那么就要寻找初始化和更新状态的规律。

初始化规律:所有持有状态下(状态0、状态2、...)的初始值都是 - prices[0]
更新状态规律:当天后面的状态与昨天前面的状态有关,需要倒序更新所有状态,防止更新后的新状态覆盖昨天的状态,注意dp[0]要单独处理。

class Solution {
    public int maxProfit(int k, int[] prices) {
        // 注意:交易k次,有k*2个状态
        int[] dp = new int[k*2]; 
        // 初始化
        for (int i = 0; i < k*2; i++){
            if (i % 2 == 0) dp[i] = -prices[0];
        }

        // 滚动更新每一天的所有状态
        for (int i = 1; i < prices.length; i++){
            // 倒序更新所有状态
            for (int j = k*2-1; j > 0; j--){
                int tag = j % 2 == 1 ? 1 : -1;
                dp[j] = Math.max(dp[j], dp[j-1] + tag * prices[i]);
            }
            // 只有dp[0]更新需要特殊单独处理
            dp[0] = Math.max(dp[0], -prices[i]); 
        }
        return dp[k*2-1];
    }
}
  • 时间复杂度:O(n),prices数组的长度
  • 空间复杂度:O(k),dp数组的长度为2k


309. 买卖股票的最佳时机含冷冻期】中等题

方法一  动态规划(二维数组)

思路:含冷冻期,即不持有状态要分为【处于冷冻期】和【不处于冷冻期】,加上持有状态,一共3种状态(所有状态都是指交易时间结束之后的状态)。取最后一天的非持有状态下(即状态2和状态3)的剩余金额的最大值即题目所求的最大利润。


步骤:

1、确定各个状态的含义

  • 状态0:今天处于持有状态,可以是今天之前就持有,也可以是今天买入才持有
  • 状态1:今天处于不持有状态,并且处于冷冻期,即只能今天卖出,那昨天要持有。
  • 状态2:今天处于不持有状态,并且处于非冷冻期,可以是昨天就处于非冷冻期,也可以是昨天处于冷冻期。

2、确定递推关系

  • dp[i][0] = Math.max(dp[i-1][0], dp[i-1][2] - prices[i])
    • 今天之前就持有:dp[i][0] = dp[i-1][0]
    • 今天买入才持有(昨天非冷冻期):dp[i][0] = dp[i-1][2] - prices[i]
  • dp[i][1] = dp[i-1][0] + prices[i]
    • 只能今天卖出(昨天持有):dp[i-1][0] + prices[i]
  • dp[i][2] = Math.max(dp[i-1][2], dp[i-1][1])
    • 昨天处于非冷冻期:dp[i][2] = dp[i-1][2]
    • 昨天处于冷冻期:dp[i][2] = dp[i-1][1]

3、确定初始值

dp[0][0] = -prices[0]:第0天处于持有状态,只能是第0天买入了

dp[0][1] = 0, dp[0][2] = 0:不存在第0天之前买入,所以不能卖出,金额为0

class Solution {
    public int maxProfit(int[] prices) {
        int[][] dp = new int[prices.length][3];
        // 初始化
        dp[0][0] = -prices[0]; // 持有状态
        dp[0][1] = 0; // 不持有状态(冷冻期)
        dp[0][2] = 0; // 不持有状态(非冷冻期)
        // 递推关系
        for (int i = 1; i < prices.length; i++){
            dp[i][0] = Math.max(dp[i-1][0], dp[i-1][2] - prices[i]); // 今天之前持有或今天买入才持有
            dp[i][1] = dp[i-1][0] + prices[i]; // 只能今天卖出,那必须昨天持有,交易时间结束后处于冷冻期
            dp[i][2] = Math.max(dp[i-1][2], dp[i-1][1]); // 昨天非冷冻期或冷冻期,今天都是非冷冻期
        }
        return Math.max(dp[prices.length-1][1], dp[prices.length-1][2]); // 最后一天的最大利润一定出现在不持有状态中的一种
    }
}
  • 时间复杂度:O(n),for循环遍历一次
  • 空间复杂度:O(n),二维数组第二个维度固定为3,第一个维度与输入数组的长度有关

注意:
1、昨天卖出股票后,交易时间过后,才处于冷冻期,那么今天交易时间之前仍是冷冻期,交易时间之后就处于非冷冻期,这样刚好冷冻了一天。

2、对于持有状态,今天买入只能是昨天处于非冷冻期(状态2),因为昨天处于冷冻期,则交易时间结束之前还是处于冷冻期,无法买入。

方法二  动态规划(一维滚动数组)

思路:优化空间复杂度O(n) -> O(1),由方法一的递推关系可以看出,

今天的状态0:与昨天的状态0和昨天的状态2有关

今天的状态1:只与昨天的状态0有关

今天的状态2:与昨天的状态2和昨天的状态1有关

由于三个状态之间相互依赖,只能用一个额外的变量记住昨天状态1的值,然后先更新今天的状态1,再更新状态0,最后更新状态2。

class Solution {
    public int maxProfit(int[] prices) {
        int[] dp = new int[3];
        // 初始化
        dp[0] = -prices[0]; // 持有状态
        dp[1] = 0; // 不持有状态(冷冻期)
        dp[2] = 0; // 不持有状态(非冷冻期)
        // 递推关系
        for (int i = 1; i < prices.length; i++){
            int tmp = dp[1];
            dp[1] = dp[0] + prices[i]; // dp[0]是昨天的状态0
            dp[0] = Math.max(dp[0], dp[2] - prices[i]); // dp[0]和dp[2]都是昨天的状态
            dp[2] = Math.max(dp[2], tmp); // 现在的dp[1]是今天的状态1,tmp才是昨天的状态1
        }
        return Math.max(dp[1], dp[2]); // 最后一天的最大利润一定出现在不持有状态中的一种
    }
}
  • 时间复杂度:O(n),for循环遍历一次
  • 空间复杂度:O(1),一维数组长度固定为3,额外的变量始终只有1个,与输入数组长度无关

714. 买卖股票的最佳时机含手续费中等题

思路:与122题基本一致,都是无限次交易次数,但区别在于含手续费,即今天卖出时,剩余金额还需要减去手续费。

class Solution {
    public int maxProfit(int[] prices, int fee) {
        int stat0 = -prices[0];
        int stat1 = 0;

        for (int i = 1; i < prices.length; i++){
            int tmp = stat0;
            stat0 = Math.max(stat0, stat1 - prices[i]);
            stat1 = Math.max(stat1, tmp + prices[i] - fee);
        }
        return stat1;
    }
}
  • 时间复杂度:O(n),for循环遍历一次
  • 空间复杂度:O(1),只使用了三个额外的变量

【买卖股票专题总结】

  • 23
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值