代码随想录算法训练营第41天 | LeetCode121.买卖股票的最佳时机、LeetCode122.买卖股票的最佳时机II、LeetCode123.买卖股票的最佳时机III

目录

LeetCode121.买卖股票的最佳时机

1. 贪心算法

2. 动态规划

LeetCode122.买卖股票的最佳时机II

1. 贪心算法

 2. 动态规划

LeetCode123.买卖股票的最佳时机III


LeetCode121.买卖股票的最佳时机

给定一个数组 prices ,它的第 i 个元素 prices[i] 表示一支给定股票第 i 天的价格。

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

返回你可以从这笔交易中获取的最大利润。如果你不能获取任何利润,返回 0

思路:今天讲解股票系列问题。

这里是需要从一个标记价格的天数范围内,先买入,再卖出,求最大利润是多少。

这里介绍两种方法。

1. 贪心算法

按照贪心的想法,那么我只要找到这个范围内的最小值购入,最大值卖出即能收获到最大利润。

但是需要注意,一定要先购入才能卖出,存在顺序问题,不是简单的找最小,找最大,两者相减即是答案,这样是错误的,比如有[7,1,5],没有办法说最大利润是7-1等于6,因为最小值在最大值右边了。

因此需要找到左边的最小值,右边的最大值,两者相减才是最终最大利润!

所以设置一个low记录左边最小值,result记录最终的最大利润,遍历完成后即得最终结果。

    int maxProfit(vector<int>& prices) {
        int low = INT_MAX;//记录购入的最小值
        int result = 0;
        for(int i = 0; i < prices.size(); i ++){
            low = min(low, prices[i]);//取最小的值购入
            result = max(result, prices[i] - low);//取最大值卖出
            //这样就能保证这样一入一出后利润最大
        }
        return result;   
    }

时间复杂度:O(n)

空间复杂度:O(1)

2. 动态规划

使用动态规划的话,比较好记录前面的情况状态。

这里使用dp[i][0]表示没有持有股票时的最大利润,dp[i][1]表示持有股票的时候的最大利润。

每一天都有这样大小为2的数组记录状态,这样就能在递推中得到最终结果。

当第i天不持有股票的时候,那么它可以保持前面不购入的状态dp[i-1][0],或者是前面一天购入的情况下,在第i天卖出,dp[i-1][1]+prices[i],同样是第i天没有持有股票,所求为最大利润,所以每次都是取两者中的最大值;

同样,当第i天持有股票的时候,那么它可以保持前面购入的状态dp[i-1][1],或者是前面一天不持有股票的情况下,第i天买入,dp[i-1][0]-prices[i],同样是第i天持有股票的情况,两者取最大即可。

最后需要说明的是,最后不持有股票一定是比还持有股票赚得多的。因为一旦持有而没有将其卖出,大概率是亏本的,利润不是最大的。所以最后返回的是dp[prices.size()-1][0]。

    int maxProfit(vector<int>& prices) {
        //dp[i][0]表示没有持有股票时的最大利润,
        //dp[i][1]表示持有股票时的最大利润
        vector<vector<int>> dp(prices.size(), vector<int>(2, 0));
        dp[0][0] = 0;
        dp[0][1] = - prices[0];
        for(int i = 1; i < prices.size(); i ++){
            dp[i][0] = max(dp[i - 1][0], dp[i - 1][1] + prices[i]);//没有持有股票时,可以是保持之前没有持有股票时的状态,也可以是前一个持有股票状态变换到第i天的时候卖出时的状态
            dp[i][1] = max(dp[i - 1][1], - prices[i]);//持有股票时,可以是保持前面持有的状态,或者是购入第i件商品
        }
        return dp[prices.size() - 1][0];//最后卖出的状态一定是比手持股票的利润要大
    }

时间复杂度:O(n)

空间复杂度:O(n)

当然,可以对上面动态规划进行空间的优化,因为整个循环都是在维护当前的与前一个的关系,所以可以尝试使用一个2x2的滚动数组进行状态的记录。这样是可以减少空间的消耗。

    int maxProfit(vector<int>& prices) {
        //dp[i][0]表示没有持有股票时的最大利润,
        //dp[i][1]表示持有股票时的最大利润
        vector<vector<int>> dp(2, vector<int>(2, 0));
        dp[0][0] = 0;
        dp[0][1] = - prices[0];
        for(int i = 1; i < prices.size(); i ++){
            dp[i % 2][0] = max(dp[(i - 1) % 2][0], dp[(i - 1) % 2][1] + prices[i]);
            dp[i % 2][1] = max(dp[(i - 1) % 2][1], - prices[i]);
        }
        return dp[(prices.size() - 1) % 2][0];//最后卖出的状态一定是比手持股票的利润要大
    }

时间复杂度:O(n)

空间复杂度:O(1)

LeetCode122.买卖股票的最佳时机II

给你一个整数数组 prices ,其中 prices[i] 表示某支股票第 i 天的价格。

在每一天,你可以决定是否购买和/或出售股票。你在任何时候 最多 只能持有 一股 股票。你也可以先购买,然后在 同一天 出售。

返回 你能获得的 最大 利润 。

思路:这道题与上面的题的区别就在于每一天都可以进行买入卖出,不再仅仅是只能买入卖出仅一次。

同样这里介绍两种方法。

1. 贪心算法

之前其实在贪心系列的时候做过这道题,因为现在每天都可以选择买入卖出了,那么局部最优就是找到产生的正利润,每次只加正利润,这样最后的整体最优就是所有正利润之后,这也就是最大利润。

    int maxProfit(vector<int>& prices) {
        int count = 0;//统计总的正利润
        for(int i = 1; i < prices.size(); i ++){
            //只统计正的利润,最后总和就是最大利润
            if(prices[i] > prices[i - 1]) count += prices[i] - prices[i - 1];
        }
        return count;
    }

时间复杂度:O(n)

空间复杂度:O(1)

 2. 动态规划

动态规划的整体思路其实和上面的题几乎完全一样,唯一不同的是对于dp[i][1]的递推公式。

dp[i][1]=max(dp[i-1][1],dp[i-1][0]-prices[i])

之所以会有加粗的部分,就是因为现在买入卖出的次数不止一次了,也许在进行这次买入股票时,前面已经完成过了买入卖出的操作,积累了一些利润了,所以在这里是需要将其加上的。

这也是与第一题的区别所在。

    int maxProfit(vector<int>& prices) {
        //dp[i][0]表示没有持有股票时的最大利润
        //dp[i][1]表示持有股票时的最大利润
        vector<vector<int>> dp(prices.size(), vector<int>(2, 0));
        dp[0][0] = 0;
        dp[0][1] = - prices[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]);
            //这里和上一题的主要区别在于每天都可以买入卖出,买入卖出不再局限于各仅一次
            //也就是说可能前面买入卖出已经有了利润dp[i - 1][0],所以需要在这里使用前面的状态
        }
        return dp[prices.size() - 1][0];
    }

时间复杂度:O(n)

空间复杂度:O(n)

当然还是可以进行空间的优化,节省空间的消耗。

    int maxProfit(vector<int>& prices) {
        //dp[i][0]表示没有持有股票时的最大利润
        //dp[i][1]表示持有股票时的最大利润
        vector<vector<int>> dp(2, vector<int>(2, 0));
        dp[0][0] = 0;
        dp[0][1] = - prices[0];
        for(int i = 1; i < prices.size(); i ++){
            dp[i % 2][0] = max(dp[(i - 1) % 2][0], dp[(i - 1) % 2][1] + prices[i]);
            dp[i % 2][1] = max(dp[(i - 1) % 2][1], dp[(i - 1) % 2][0] - prices[i]);
        }
        return dp[(prices.size() - 1) % 2][0];
    }

时间复杂度:O(n)

空间复杂度:O(1)

LeetCode123.买卖股票的最佳时机III

给定一个数组,它的第 i 个元素是一支给定的股票在第 i 天的价格。

设计一个算法来计算你所能获取的最大利润。你最多可以完成 两笔 交易。

注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。

思路:这道题相对于前面两道题稍微就有点难了。

因为它要考虑的状态多了。最多完成两笔交易,或许完成两笔,或许完成一笔,或许什么操作也不做。

于是,我们这里设置了5个状态,0表示无操作,1表示第一次买入股票,2表示第一次卖出股票,3表示第二次买入股票,4表示第二次卖出股票。

这里的买入或者卖出不是指在当时的i时必须要买入或者卖出,也可以保持前一个买入或者卖出的状态,理解了这一点,这道题也就变得简单起来了。

为什么这么说呢?

因为你会发现这些状态的递推公式及其相似,和我们之前做过的两道题的主题逻辑是一样的。

如果想要在第i天购入股票,那么就可以选择保持为前一个已经持有股票的状态,也可以选择在前一个没有持有股票的状态下,买入股票,取两者的最大值即可;

同样,想要在第i天卖出股票,那么就可以选择保持为前一个已经卖出,没有持有股票的状态,也可以选择在前一个持有股票的状态下,卖出股票,取两者的最大值即可。

所以这样看下来其实就简单了。

这里还是需要说明一点,最后一定是找卖出的状态下的利润,那这里有两个,一个是第一次卖出,一个是第二次卖出,该选谁呢?

应该选第二次卖出的状态。因为两次的买入卖出的利润一定是大于等于一次的买入卖出的,这里求的就是最大值,所以不会出现第二次买入卖出还会比第一次低的。

有人说,万一第一次卖出就是最大值了呢?那其实可以这样理解,在第一次卖出就是利润最大值的当天,买入卖出,相当于其实还是第一次卖出的最大值,所以第二次卖出后的最大利润是包含了第一次卖出的最大利润的。因此这里选择返回第二次卖出的结果。

    int maxProfit(vector<int>& prices) {
        //这里设置5个状态
        //0表示没有任何操作,1表示第1次买入,2表示第1次卖出,3表示第2次买入,4表示第2次卖出
        vector<vector<int>> dp(prices.size(), vector<int>(5, 0));
        dp[0][0] = 0;
        dp[0][1] = - prices[0];
        dp[0][2] = 0;
        dp[0][3] = - prices[0];
        dp[0][4] = 0;
        for(int i = 1; i < prices.size(); i ++){
            dp[i][0] = dp[i - 1][0];
            dp[i][1] = max(dp[i - 1][1], dp[i - 1][0] - prices[i]);
            dp[i][2] = max(dp[i - 1][2], dp[i - 1][1] + prices[i]);
            dp[i][3] = max(dp[i - 1][3], dp[i - 1][2] - prices[i]);
            dp[i][4] = max(dp[i - 1][4], dp[i - 1][3] + prices[i]);
        }
        return dp[prices.size() - 1][4];//两次买入卖出后,该价值一定是最大的
    }

时间复杂度:O(n)

空间复杂度:O(n)

这里其实可以不用设置状态为0的数组,因为不操作本身就是0了,如果操作后有正利润,自然就会将0覆盖,所以可以不用设置状态为0的数组。

    int maxProfit(vector<int>& prices) {
        //这里设置4个状态
        //0表示第1次买入,1表示第1次卖出,2表示第2次买入,3表示第2次卖出
        vector<vector<int>> dp(prices.size(), vector<int>(4, 0));
        dp[0][0] = - prices[0];
        dp[0][1] = 0;
        dp[0][2] = - prices[0];
        dp[0][3] = 0;//初始化,这里第0天的初始化可以看作当天买入卖出,相当于没赚也没亏
        for(int i = 1; i < prices.size(); i ++){
            dp[i][0] = max(dp[i - 1][0], - prices[i]);
            dp[i][1] = max(dp[i - 1][1], dp[i - 1][0] + prices[i]);
            dp[i][2] = max(dp[i - 1][2], dp[i - 1][1] - prices[i]);
            dp[i][3] = max(dp[i - 1][3], dp[i - 1][2] + prices[i]);
        }
        return dp[prices.size() - 1][3];//两次买入卖出后,该价值一定是最大的
    }

这里还可以使用一个一维滚动数组来优化空间消耗,减少空间复杂度。

    int maxProfit(vector<int>& prices) {
        //这里设置4个状态
        //0表示第1次买入,1表示第1次卖出,2表示第2次买入,3表示第2次卖出
        vector<int> dp(4, 0);
        dp[0] = - prices[0];
        dp[1] = 0;
        dp[2] = - prices[0];
        dp[3] = 0;//初始化,这里第0天的初始化可以看作当天买入卖出,相当于没赚也没亏
        for(int i = 1; i < prices.size(); i ++){
            dp[0] = max(dp[0], - prices[i]);
            dp[1] = max(dp[1], dp[0] + prices[i]);
            dp[2] = max(dp[2], dp[1] - prices[i]);
            dp[3] = max(dp[3], dp[2] + prices[i]);
        }
        return dp[3];//两次买入卖出后,该价值一定是最大的
    }

时间复杂度:O(n)

空间复杂度:O(1)

感谢你的阅读,希望我的文章能够给你帮助,如果有帮助,麻烦点赞加收藏,或者点点关注,非常感谢。

如果有什么问题欢迎评论区讨论!

  • 26
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
第二十二算法训练营主要涵盖了Leetcode题目中的三道题目,分别是Leetcode 28 "Find the Index of the First Occurrence in a String",Leetcode 977 "有序数组的平方",和Leetcode 209 "长度最小的子数组"。 首先是Leetcode 28题,题目要求在给定的字符串中找到第一个出现的字符的索引。思路是使用双指针来遍历字符串,一个指向字符串的开头,另一个指向字符串的结尾。通过比较两个指针所指向的字符是否相等来判断是否找到了第一个出现的字符。具体实现的代码如下: ```python def findIndex(self, s: str) -> int: left = 0 right = len(s) - 1 while left <= right: if s[left == s[right]: return left left += 1 right -= 1 return -1 ``` 接下来是Leetcode 977题,题目要求对给定的有序数组中的元素进行平方,并按照非递减的顺序返回结果。这里由于数组已经是有序的,所以可以使用双指针的方法来解决问题。一个指针指向数组的开头,另一个指针指向数组的末尾。通过比较两个指针所指向的元素的绝对值的大小来确定哪个元素的平方应该放在结果数组的末尾。具体实现的代码如下: ```python def sortedSquares(self, nums: List[int]) -> List[int]: left = 0 right = len(nums) - 1 ans = [] while left <= right: if abs(nums[left]) >= abs(nums[right]): ans.append(nums[left ** 2) left += 1 else: ans.append(nums[right ** 2) right -= 1 return ans[::-1] ``` 最后是Leetcode 209题,题目要求在给定的数组中找到长度最小的子数组,
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值