题目描述:
在股市的交易日中,假设最多可进行两次买卖(即买和卖的次数均小于等于2),规则是必须一笔成交后进行另一笔(即买-卖-买-卖的顺序进行)。给出一天中的股票变化序列,请写一个程序计算一天可以获得的最大收益。请采用实践复杂度低的方法实现。
给定价格序列prices及它的长度n,请返回最大收益。保证长度小于等于500。
测试用例:
[10,22,5,75,65,80],6
返回结果:
87
理解误区:
这道题有一个理解误区就是,如果是第i天卖出,那么是否可以在第i天接着买入,还是必须要在第i+1天的时候才能买入进行第二笔交易?我最初理解的是后者,但是后来自习想了想应该是前者
我的思路分析:
当时想着构造一个二维矩阵,profit[i][j]指的就是从第i+1天买入,到第j+1天(数组下标从0开始)卖出的收益,然后把所有的一次交易的收益全部求出来,这样,在这个二维数组里,对角线以上的部分会存有信息,但是对角线以及对角线以下的部分不会存有信息。然后求出一次交易的最大值。
之后开始求两次交易,两次交易的话,先遍历二维数组中已经求好的一次交易的收益,选择那些收益为正的,记录,比如说遍历到了profit[3][5]是一个正数,表示第4次买入,第6次卖出是有收益的(数组下表从0开始),那么接下来所需要做的,就是遍历从第7次到最后一次之间的交易记录的利润最大值,两者相加,然后和一次交易记录的最大值进行比较。
我的代码:
/**
* 求最大收益,交易次数小于等于两次
* @param prices 每一天的股票价格
* @param n 天数
* @return 返回小于等于两次交易所获取的最大利润
*/
public static int maxProfit(int[] prices, int n) {
int[][] profit = new int[n][n];
int max = Integer.MIN_VALUE;
//对角线无效,对角线以下的三角形无效
//记录一次交易所获取的利润
for(int i = 0;i<n;i++){
for(int j = i + 1;j<n;j++){
profit[i][j] = prices[j] - prices[i];
//保存一次交易的最大值
max = Math.max(profit[i][j], max);
}
}
//遍历数组,计算两次交易的利润最大值
for(int i = 0;i<n;i++){
for(int j = i+1;j<n;j++){
//只有当一次交易的利润是正的的时候,才会考虑计算第二次利润
if(profit[i][j]>0){
//在后续的日期里计算第二次交易的最大值
for(int p = j+1;p<n;p++){
//第i天卖出之后,第i天可以接着买入,所以这里用j+1而不是j+2
for(int q = j+1;q<n;q++){
//负利润被忽略
if(profit[p][q]<0)
continue;
else{
//计算两次交易的利润和
int sum = profit[i][j] + profit[p][q];
//与最大利润进行比较
max = Math.max(sum, max);
}
}
}
}
}
}
return max;
}
出现的问题:
但是我的代码提交上去之后,发现,对于计算小的数组还是可以的,但是随着数组规模的扩大,时间就会花费很久,所以算法的性能不好,需要优化,其实也是,自己的代码存在至少两个缺点吧,第一,四层for循环嵌套,当n的规模变大的时候,就会比较耗时。二,没有利用动态规划的思想,通过做这道题,我领悟出了,动态规划的思想精髓其实是利用之前的决策结果,进行本次的决策,也就是说,动态规划的思想,是要求一定要使用之前的数据的,这样计算起来因为有之前的数据了,所以速度回很快。
改进思路:
自己看了一下网上的其他人的解法,发现果然很赞。别人的思路是这样的。用两个数组,一个数组preProfit[i],指的是第i+1天(数组下标从0开始)之前,当然也包括第i+1天的最大收益,需要保存的一个数据是第i+1天之前的最小价格,如果第i+1天的价格减去最小价格后的利润是要比不在这天卖出的利润大,那么就果断卖出,否则,这一天就不卖出,那么这天之前的最大利润和昨天的最大利润是一样的。这样就使用了昨天的数据,这才是正统的动态规划的思想。
另外一个数组postProfit[i]指定是第i天买入的话,之后所能获得的最大利润,需要保持一个变量记录第i+1天之后的最大价格,如果最大价格减去这一天的利润比明天之后卖出的最大利润要大的话,就卖出,否则,就等于明天之后卖出的最大利润。最后将两个数组求和,计算出总的最大利润
改进之后的代码:
/**
* 求最大收益,交易次数小于等于两次
* @param prices 每一天的股票价格
* @param n 天数
* @return 返回小于等于两次交易所获取的最大利润
*/
public static int maxProfit2(int[] prices, int n) {
//第i天之前的最大利益
int[] preProfit = new int[n];
//第i天之后的最大
int[] postProfit = new int[n];
//总的最大利润
int max = Integer.MIN_VALUE;
//如果今天的价格减掉最小价格比截止到昨天的最大收益大,就用今天的价格减去最小价格,否则,用截止到昨天的最大收益
int minBuy = prices[0];
for(int i = 1;i<n;i++){
minBuy = Math.min(minBuy, prices[i]);
preProfit[i] = Math.max(preProfit[i-1], prices[i] - minBuy);
}
//如果最大价格减掉今天价格比明天以后买入的最大收益大,就用最大价格减掉今天价格,否则,用明天以后买入的最大收益
int maxSell = prices[n-1];
for(int i = n-2;i>=0;i--){
maxSell = Math.max(maxSell, prices[i]);
postProfit[i] = Math.max(postProfit[i+1], maxSell-prices[i]);
}
//求出两次交易的和,与总的最大利润进行比较
for(int i = 0;i<n;i++){
max = Math.max(preProfit[i] + postProfit[i], max);
}
return max;
}
比较:
使用下面的算法的话,速度要快很多,当n是100的时候,我原来的那个算法是需要36毫秒,但是下边的算法只需要0毫秒
当n是500的时候,我原来的算法需要4428毫秒,接近4秒钟,显然对于这道题来说,就超时了,但是下边的算法只需要1毫秒!动态规划的威力可见一斑!!!