1、问题描述
给定一个数组,数组中的第i个元素表示一支给定股票第i天的价格。
如果你最多只许完成一笔交易,即只能买入一次和卖出一次,
设计一个算法来计算你能获得的最大收益。
注意,你不能在买入股票前卖出股票。
示例1:
输入:[7,1,2,5,3,6]
输出:5
解释:在第2天的时候买入(股票价格为1),第6天的时候卖出(股票价格为6),可以获得最大收益6-1 = 5.
示例2:
输入:[7,6,4,3,1]
输出:0 。
解释:当天买入,当天卖出不会出现亏损。
2、解题思路
解决这个问题有以下几种方法:
- 方法1:暴力法。使用指针i、j双层循环,外部循环i表示在第i天的时候买入,内部循环j表示在第j天的时候就卖出,根据要求,j必须大于i,因此有i的遍历范围为1到数组长度,j的遍历范围为i+1到数组长度。很明显,这种方法的时间复杂度为 O ( n 2 ) O(n^2) O(n2),空间复杂度为 O ( 1 ) O(1) O(1)。
- 方法2:动态规划。这个方法有两个关键变量,maxprofit和minprice。
maxpofit:表示到第j天之前获得的最大收益;
minprice:记录第j天之前,股票的最低价格;
(1) j从头开始遍历价格数组prices:
(2)如果在第j天卖出时获得的收益大于maxprofit,则更新 maxprofit,即maxprofit = max(maxprofit,prices[j] - minprice);
(3)如果第j天股票的价格prices[j]小于minprice,则更新minprices,即minprice =min(minprice,prices[j]);
这种方法的时间复杂度为 O ( n ) O(n) O(n),空间复杂度为 O ( 1 ) O(1) O(1).
- 方法3:转换成求最大连续子数组的问题。
求出价格数组相邻两天价格的差值,得到一个新的差值数组。
然后在差值数组上求最大连续子数组的和即为可获得的最大收益。
这种方法的基本思想是,假设你在重复这样的操作,不断地在第i天买入,第i+1天卖出,求累积的收益。
这种方法的时间复杂度为 O ( n ) O(n) O(n),空间复杂度为 O ( n ) O(n) O(n).
3、代码实现
方法2:
class Solution {
public:
int maxProfit(vector<int>& prices) {
if(prices.size() == 0 || prices.size() == 1){
return 0;
}
int maxprofit = 0;
int minprice = prices[0];
for(int j = 0; j < prices.size(); j++){
maxprofit = max(maxprofit, prices[j] - minprice);
minprice = min(minprice, prices[j]);
}
return maxprofit;
}
};
方法2:
/*方法1:
思想:转换成求最大连续子数组的问题,在原数组上求相邻两个元素的差值得到一个新的数组,然后在新的数组上求最大连续子数组。
*/
class Solution {
public:
int maxProfit(vector<int>& prices) {
if(prices.size() == 0 || prices.size() == 1){
return 0;
}
vector<int> gain;
for(int i = 0; i < prices.size() - 1; i++){
gain.push_back(prices[i+1] - prices[i]);
}
int ngreatest = gain[0];
int cursum = gain[0];
for(int j = 1; j < gain.size(); j++){
if(cursum < 0){
cursum = gain[j];
}
else{
cursum += gain[j];
}
if(cursum > ngreatest){
ngreatest = cursum;
}
}
ngreatest = max(ngreatest, 0);
return ngreatest;
}
};
4、拓展问题
拓展1: 如果允许多次交易,即多次买入和卖出一支股票,求能获得的最大利润。
问题链接:122. 买卖股票的最佳时机 II
分析:这一拓展问题的难点在于,要获得最大利润,交易的次数是未知的。解决这道问题有以下几种方法:
方法1:暴力搜索。使用这种方法时,我们需要计算所有可能的交易组合相对应的最大利润。交易组合的产生是通过在每一天,根据当天是否持有股票来选择相应的操作(操作的种类包括不操作、买入、卖出)、交易组合所对应的解空间树如下图所示:
代码实现:
/*算法思想:
深度优先遍历交易组合的解空间树
*/
class Solution {
int maxprofit;
public:
int maxProfit(vector<int>& prices) {
if(prices.size() < 2){
return 0;
}
int status = 0;
int curprofit = 0;
// int maxprofit = 0;
int day = 0;
maxprofit = 0;
dfs(prices, day, status, curprofit);
return maxprofit;
}
/*day:表示搜索的第几天
status: 表示当前是否持有股票;
curprofit:搜索到当前节点的利益和;
maxprofit: 最大利益*/
void dfs(vector<int>& prices, int day, int status, int curprofit){
// cout<<"day="<<day<<endl;
if(day == prices.size()){
maxprofit = max(maxprofit, curprofit);
return ;
}
/*保持当前状态,没有股票仍然不买入,有股票不卖出*/
dfs(prices, day + 1, status, curprofit);
/*如果当天没有股票,尝试买入股票,否则尝试卖出股票*/
if(status == 0){
dfs(prices, day + 1, 1, curprofit - prices[day]);
}
else{
dfs(prices, day + 1, 0, curprofit + prices[day]);
}
}
};
方法2:动态规划。
(1) 定义状态:
根据交易组合的解空间树,我们可以使用dp[i][j]来表示到第i天为止,持有股票状态为j时的所获得的最大收益。
i的取值范围为[0,prices.length - 1];
j的取值范围为{0, 1},1表示有股票,0表示没有股票。
(2) 确定状态转移方程
- 状态从持有现金开始,到最后一天我们关心的状态仍然是持有现金
- 每一天的状态可以保持不变,也可以转移,状态转移用下图表示:
- 因为交易次数不受限制,因此除了最后一天,每一天都可能的状态都可能保持不变,也可能转移。
- 经过以上分析:状态转移方程如下:
d p [ i ] [ j ] = { m a x { d p [ i − 1 ] [ 0 ] 、 d p [ i − 1 ] [ 1 ] + p r i c e s [ i ] } j = 0 ; m a x { d p [ i − 1 ] [ 1 ] 、 d p [ i − 1