题目链接:
题目描述:
买卖股票的最佳时机
给定一个数组,它的第 i 个元素是一支给定股票第 i 天的价格。
如果你最多只允许完成一笔交易(即买入和卖出一支股票一次),设计一个算法来计算你所能获取的最大利润。
注意:你不能在买入股票前卖出股票。
示例 1:
输入: [7,1,5,3,6,4]
输出: 5
解释: 在第 2 天(股票价格 = 1)的时候买入,在第 5 天(股票价格 = 6)的时候卖出,最大利润 = 6-1 = 5 。
注意利润不能是 7-1 = 6, 因为卖出价格需要大于买入价格;同时,你不能在买入前卖出股票。
示例 2:
输入: [7,6,4,3,1]
输出: 0
解释: 在这种情况下, 没有交易完成, 所以最大利润为 0。
通过次数199,984 提交次数370,510
解决方案:
先从题目出发进行理解,如果是第 i 天卖出股票,那在这之前必须有 < prices[i] 的,想要得到最大利润,要找出前 i-1 天中小于 prices[i] 的最小值。显然,如果对每一天都去找之前比它便宜的最小值,会有很多重复计算。
而动态规划会把已经计算过的结果保存下来,避免重复计算,且该题符合动态规划的条件。
设 dp[i] 为第 i 天卖出获得的最大利润。
每天的价格与前一天对比,只会有三种情况:
如果当天价格和前一天相同,即 prices[i] = prices[i-1],那么第 i 天卖出能拿到的最大利润和前一天卖出相同,有 dp[i] = dp[i-1];
如果当天价格高于前一天,由于 dp[i-1] 代表第 i-1 天卖出的最大利润,那么第 i 天卖出的最大利润就等于前一天卖出的最大利润加上两天的价格差,即 dp[i] = prices[i] - prices[i-1] + dp[i-1];
如果当天价格低于前一天,就要考虑两种情况:第一种,虽然利润少,但还是有利润,即 dp[i-1] > prices[i-1] - prices[i],那么 dp[i] = dp[i-1] - (prices[i-1] - prices[i]);第二种,前面所有的价格都比当天的要高,没有利润,根据题意这种情况就不买,也就是利润为 0,即 dp[i] = 0(可以理解为,前一天的最大利润比这两天的价格差值还要小,dp[i-1] - (prices[i-1] - prices[i] < 0,就不卖出了)。
代码:
/*
*动态规划:设dp[i]为第i天卖出获得的最大利润
*/
class Solution {
public int maxProfit(int[] prices) {
if(prices.length < 2) return 0;//空数组或者只有一个元素,没有利润
int[] dp = new int[prices.length];
dp[0] = 0;//第一天卖出最高利润为0
int result = 0;
for(int i = 1; i < prices.length; i++){
if(prices[i] == prices[i-1]){
dp[i] = dp[i-1];
}else if(prices[i] > prices[i-1]){
dp[i] = prices[i] - prices[i-1] + dp[i-1];
if(result < dp[i]){ result = dp[i];}
}else{
if(dp[i-1] > prices[i-1] - prices[i]){
dp[i] = dp[i-1] - (prices[i-1] - prices[i]);
}
}
}
return result;
}
}
针对空间复杂度进行优化
动态规划在空间复杂度上的优化思想大同小异,我们每次计算 dp[i] 都只用到了前面的一项,所以可以不创建新的数组,用一个变量暂存上一个数据即可。
/*
*动态规划:设dp[i]为第i天卖出获得的最大利润
*优化:每次计算 dp[i] 其实只用到了其前一位,可以不创建数组
*/
class Solution {
public int maxProfit(int[] prices) {
if(prices.length < 2) return 0;//空数组或者只有一个元素,没有利润
int dp = 0;
int result = 0;
for(int i = 1; i < prices.length; i++){
int prev = dp;
if(prices[i] > prices[i-1]){
dp = prices[i] - prices[i-1] + dp;
if(result < dp){ result = dp;}
}else{
if(dp > prices[i-1] - prices[i]){
dp = dp - (prices[i-1] - prices[i]);
}else{ dp = 0;}
}
}
return result;
}
}
提交结果如下:
我们其实没有改变时间复杂度,只是把空间复杂度降低了,为什么会得到这样的执行结果呢?
首先,关于时间复杂度,由于本身用时很短,只有1~2ms,这个时候即使时间复杂度相同,你访问的次数多点儿少点儿都可能对结果产生很大的影响,可能一个 dp = a + b 和 dp = dp + a + b 的时间差就能让总的执行时间发生改变。
然后是空间复杂度,理论上我们把空间复杂度从 O(n) 降到了 O(1),但是LeetCode上的内存消耗不只是你的程序开辟的空间,测试用例也是占内存的,以这道题为例,通过了 200 个测试用例,每个测试用例都是一个数组,我们对空间的优化只产生了 0.x MB 的差距,个人觉得把空间复杂度优化到 O(1) 就足够了,不用过于在意内存消耗击败了多少人…
最后是关于动态规划思想的一点儿感悟,动态规划思想更像是用空间去换时间,我们不想去重复计算已经计算过的数据,那就把这个数据保存下来,这可能也是很多题目采用动态规划思想时,执行用时往往能击败多数用户的原因。再就是,既然我们想避免重复的运算,在思考问题的时候就要多去想想,怎么才能在计算时用上之前的计算,也就是推导状态转移方程式的过程。最后,推导过程中一定要注意把各种情况全覆盖,比如本题分为了:prices[i] = prices[i-1];prices[i] > prices[i-1];prices[i] < prices[i-1],这已经是这两者关系的全部可能性了,如果你的想法很难去覆盖所有的情况,可以尝试换个思路,路就能走宽了。