leetcode 121. 买卖股票的最佳时机---九种解法
Posted 大忽悠爱忽悠
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了leetcode 121. 买卖股票的最佳时机---九种解法相关的知识,希望对你有一定的参考价值。
1.暴力法:
- 我们需要找出给定数组中两个数字之间的最大差值(即,最大利润)。此外,第二个数字(卖出价格)必须大于第一个数字(买入价格)。
class Solution {
public:
int maxProfit(vector<int>& prices)
{
int n = prices.size();
int maxProfit = 0;
for (int i = 0; i < n; i++)
{
for (int j = i+1; j < n; j++)
{
maxProfit = max(maxProfit, prices[j] - prices[i + 1]);
}
}
return maxProfit;
}
};
2.一次遍历
- 从上面的图片我们可以看出,我们在每个节点其实只会做两件事(第一个节点除外,只能买入不能卖出),这两件事分别是:买入或卖出。那么我们其实可以用一个循环来计算出最大的利润,我们只需要依次对于每个节点做以下两个判断:
- 判断当前节点是不是相对最低价,如果是,则将它设置为最低价(也就是买入);
- 如果当前节点不是最低价,那我们就将它卖出,然后计算卖出的收益(当前节点减去相对最低价),
- 如果卖出的收益大于目前的最高收益,则将此值设置为最高收益。
class Solution {
public:
int maxProfit(vector<int>& prices)
{
int maxProfit = 0;//初始最高收益为0
int lowPrice = prices[0];//假设prices[0]为最低股价
for (vector<int>::size_type i = 1; i < prices.size(); i++)
{
//如果当日股价比最低股价还低,那么更新最低股价
if (prices[i] < lowPrice)
{
lowPrice = prices[i];
}
else//如果当日股价比最低股价高,那么判断如果当日卖出股票获得的收益是否比记录在案的最高收益大
{
maxProfit = max(maxProfit, prices[i] - low】k rice);//prices[i] - lowPrice:当日,、/*】/股价卖出的股价减去买入的最低股价
}
}
return maxProfit;
}
};
3.双指针解决
- 我们还可以使用两个指针,一个指针记录访问过的最小值(注意这里是访问过的最小值),一个指针一直往后走,然后计算他们的差值,保存最大的即可,这里就以示例1为例来画个图看下
class Solution {
public:
int maxProfit(vector<int>& prices)
{
int maxPro = 0;
int Min = prices[0];
for (vector<int>::size_type i = 1; i < prices.size(); i++)
{
Min = min(Min, prices[i]);
maxPro = max(maxPro, prices[i] - Min);
}
return maxPro;
}
};
- 其实上面的双指针法和第二种一次遍历用的是一种方法。
4.单调栈解决
- 单调栈解决的原理很简单,我们要始终保持栈顶元素是所访问过的元素中最小的,如果当前元素小于栈顶元素,就让栈顶元素出栈,让当前元素入栈。如果访问的元素大于栈顶元素,就要计算他和栈顶元素的差值,我们记录最大的即可,代码如下。
class Solution {
public:
int maxProfit(vector<int>& prices)
{
stack<int> s;
int maxPro = 0;//初始最大利润为0
s.push(prices[0]);//第一天的股价最低
for (int i = 0; i < prices.size(); i++)
{
if (prices[i] < s.top())//满足条件,更新最低股价
{
s.pop();
s.push(prices[i]);
}
else
{
maxPro = max(maxPro, prices[i] - s.top());
}
}
return maxPro;
}
};
- 仔细看下就会明白这种解法其实就是双指针的另一种实现方式,只不过双指针使用的是一个变量记录访问过的最小值,而这里使用的是栈记录的。
5.动态规划 - 思路:题目只问最大利润,没有问这几天具体哪一天买、哪一天卖,因此可以考虑使用 动态规划 的方法来解决。
- 买卖股票有约束,根据题目意思,有以下两个约束条件:
条件 1:你不能在买入股票前卖出股票;
条件 2:最多只允许完成一笔交易。
- 因此 当天是否持股 是一个很重要的因素,而当前是否持股和昨天是否持股有关系,为此我们需要把 是否持股 设计到状态数组中。
- 状态定义:
dp[i][j]:下标为 i 这一天结束的时候,手上持股状态为 j 时,我们持有的现金数。
j = 0,表示当前不持股;
j = 1,表示当前持股。
-
注意: 这个状态具有前缀性质,下标为 i 的这一天的计算结果包含了区间 [0, i] 所有的信息,因此最后输出 dp[len -1][0]。
-
说明:
-
使用「现金数」这个说法主要是为了体现 买入股票手上的现金数减少,卖出股票手上的现金数增加 这个事实;
-
「现金数」等价于题目中说的「利润」,即先买入这只股票,后买入这只股票的差价;
-
因此在刚开始的时候,我们的手上肯定是有一定现金数能够买入这只股票,即刚开始的时候现金数肯定不为 0,但是写代码的时候可以设置为0。极端情况下(股价数组为 [5, 4, 3, 2, 1]),此时不发生交易是最好的
-
推导状态转移方程:
-
dp[i][0]:规定了今天不持股,有以下两种情况:
昨天不持股,今天什么都不做;
昨天持股,今天卖出股票(现金数增加),
- dp[i][1]:规定了今天持股,有以下两种情况:
昨天持股,今天什么都不做(现金数与昨天一样);
昨天不持股,今天买入股票(注意:只允许交易一次,因此手上的现金数就是当天的股价的相反数)。
知识点:
- 多阶段决策问题:动态规划常常用于求解多阶段决策问题;
- 无后效性:每一天是否持股设计成状态变量的一维。状态设置具体,推导状态转移方程方便。
class Solution {
public:
int maxProfit(vector<int>& prices)
{
int days = prices.size();
//特殊情况,如果天数只有一天,那么最大利润为0,因为一旦买入必须卖出,并且只发生一次
if (days == 1)
{
return 0;
}
//创建一个dp二维数组
int (*dp)[2] = new int[days][2];
//初始值:第一天的状态
dp[0][0] = 0;//第一天不买入股票,那么当前现金数为0
dp[0][1] = -prices[0];//第一天买入股票,那么当前现金数为0减去第一天的股价
//从第二天开始遍历
for (int i=1;i<days;i++)
{
//如果第i天没有持股,说明第i-1天有两种状态:没有持股第二天也不买,或者持有股票第二天卖出
dp[i][0] = max(dp[i - 1][0], dp[i - 1][1]+prices[i]);
//如果第i天持股了,说明第i-1天有两种状态:持股第二天不卖,不持股买入第二天的股票
dp[i][1] = max(dp[i - 1][1],-prices[i]);
}
return dp[days- 1][0];//我们要计算最后的最大利润,即最后我们手上有多少钱,因为初始钱为0
}
};
复杂度分析:
- 时间复杂度:O(N),遍历股价数组可以得到最优解;
- 空间复杂度:O(N),状态数组的长度为 N。
6.动态规划的空间优化-----滚动数组
class Solution {
public:
int maxProfit(vector<int>& prices)
{
int days = prices.size();
//特殊情况,如果天数只有一天,那么最大利润为0,因为一旦买入必须卖出,并且只发生一次
if (days == 1)
{
return 0;
}
//创建一个dp二维数组
int (*dp)[2]= new int[2][2];
//初始值:第一天的状态
dp[0][0] = 0;//第一天不买入股票,那么当前现金数为0
dp[0][1] = -prices[0];//第一天买入股票,那么当前现金数为0减去第一天的股价
//从第二天开始遍历
for (int i=1;i<days;i++)
{
//如果第i天没有持股,说明第i-1天有两种状态:没有持股第二天也不买,或者持有股票第二天卖出
dp[i%2][0] = max(dp[(i-1)%2][0], dp[(i - 1)%2][1]+prices[i]);
//如果第i天持股了,说明第i-1天有两种状态:持股第二天不卖,不持股买入第二天的股票
dp[i%2][1] = max(dp[(i - 1)%2][1],-prices[i]);
}
return dp[(days- 1)%2][0];//我们要计算最后的最大利润,即最后我们手上有多少钱,因为初始钱为0
}
};
复杂度分析:
- 时间复杂度:O(N)O(N),遍历股价数组可以得到最优解;
- 空间复杂度:O(1)O(1),状态数组的长度为 44。
7.对状态转移方程的空间优化
- 状态转移方程里下标为 i 的行只参考下标为 i - 1 的行(即只参考上一行),并且:
下标为 i 的行并且状态为 0 的行参考了上一行状态为 0 和 1 的行;
下标为 i 的行并且状态为 1 的行只参考了上一行状态为 1的行。
class Solution {
public:
int maxProfit(vector<int>& prices)
{
int days = prices.size();
//特殊情况,如果天数只有一天,那么最大利润为0,因为一旦买入必须卖出,并且只发生一次
if (days == 1)
{
return 0;
}
int* dp = new int[2];
dp[0] = 0;
dp[1] = -prices[0];
for (int i = 0; i < days; i++)
{
dp[0] = max(dp[0], dp[1] + prices[i]);
dp[1] = max(dp[1],-prices[i]);
}
return dp[0];
}
};
8.贪心算法
- 既然只能交易一次,那么肯定是每次都要选取购入金额最小的时机,至于售出那就需要比较了,因为即使是购入金额最小的时机,售出的时候也不一定是最大利润(这就是贪心,每次都去寻找当前的最优情况),代码如下:
class Solution {
public:
int maxProfit(vector<int>& prices)
{
int len = prices.size();
int lowerPrice = prices[0];
int maxPro = 0;
for (int i=0; i < len; i++)
{
if (prices[i] < lowerPrice)
lowerPrice = prices[i];
else
maxPro = max(maxPro, prices[i] - lowerPrice);
}
return maxPro;
}
};
- 其实这里的贪心算法就是法2和法3的思路
9.参照最大子序列和的解法
- 假设数组的值是[a,b,c,d,e,f],我们用数组的前一个值减去后一个值,得到的新数组如下
- [b-a,c-b,d-c,e-d,f-e]
- 我们在新数组中随便找几个连续的数字相加就会发现一个规律,就是中间的数字都可以约掉,比如新数组中第1个到第4个数字的和是
- b-a+c-b+d-c+e-d=e-a。
- 我们来看下示例1中得到的新数组,连续的最大值就是
- 4+(-2)+3=5。
- 我们这里数组中前面一个值减去后一个值得到的最大值,变为了求后减前得到的新数组的最大子序列和的问题
class Solution {
public:
int maxProfit(vector<int>& prices)
{
int len = prices.size();
int cur = 0;
int Max = cur;
//注意i的初始值为1,因此要计算前一个元素减去后一个元素的值
for (int i = 1; i < len; i++)
{
//这里max(cur,0)是因为如果当前的cur都小于0了,那么只会越累加得到的值越小,所以直接从后一个值开始累加,相当于累加初始值重置为0
cur = max(cur, 0) + prices[i] - prices[i - 1];
Max = max(Max, cur);//判断当前的最大子序列和是否需要更新
}
return Max;
}
};
以上是关于leetcode 121. 买卖股票的最佳时机---九种解法的主要内容,如果未能解决你的问题,请参考以下文章
[JavaScript 刷题] DP - 买卖股票的最佳时机,leetcode 121