目录
【121. 买卖股票的最佳时机】简单题
方法一 暴力(超时)
class Solution {
public int maxProfit(int[] prices) {
int res = 0;
for (int i = 0; i < prices.length; i++){
for (int j = i + 1; j < prices.length; j++){
res = Math.max(res, prices[j] - prices[i]);
}
}
return res;
}
}
- 时间复杂度:O(n²),for循环嵌套遍历
- 空间复杂度:O(1)
方法二 贪心(简单)
思路:动态更新当天左边区间的最小值,获取当天的最大利润,更新全局最大利润。
class Solution {
public int maxProfit(int[] prices) {
int res = 0;
int min = Integer.MAX_VALUE;
for (int i = 0; i < prices.length; i++){
min = Math.min(min, prices[i]);
res = Math.max(res, prices[i] - min);
}
return res;
}
}
- 时间复杂度:O(n),for循环遍历一次
- 空间复杂度:O(1)
方法三 动态规划(二维数组)
步骤:
1、确定 dp[i] 的含义
对于第 i 天,有两种状态,持有和不持有,那么就需要二维数组存储每天的两个状态。
那么,可以假设:
dp[i][0] 对应第 i 天持有的状态,表示第 i 天持有股票时手上的最高剩余金额。
dp[i][1] 对应第 i 天不持有的状态,表示第 i 天不持有股票时手上的最高剩余金额。
最后,取最后一天不持有状态对应的最高剩余金额就是最大利润。
2、确定递推关系(重要前提:只能对这只股票买卖一次,默认初始金额为0)
dp[i][0]:已经持有,证明要么当天之前买入,要么刚好当天买入
- 如果当天之前就已买入,即当天不用花钱再买,那么dp[i][0] = dp[i - 1][0]
- 如果当天之前没有买入,那么当天就需要购买,那么dp[i][0] = 0 - prices[i] = - prices[i]
dp[i][1]:不持有,证明要么没买,要么当天之前卖出,要么刚好当天卖出
- 如果到当天为止都没买入,则剩余金额依然为0,dp[i][1] = 0(后面两个状态>=0,这个状态可以忽略)
- 如果当天之前就已卖出,即当天不用再卖,那么dp[i][1] = dp[i - 1][1]
- 如果当天之前没有卖出,那么需要昨天持有的情况下,当天才能卖出,那么dp[i][1] = dp[i-1][0] + prices[i]
3、确定初始值
由递推关系可以看出,当天的数据基本都是由昨天的数据推断而来,所以需要先确定第0天两个状态的取值:
- dp[0][0]:第0天是持有状态,只能是第0天买入,所以 dp[0][0] = - prices[0].
- dp[0][1]:第0天是不持有状态,则肯定只能是当天没买,所以 dp[0][1] = 0.
class Solution {
public int maxProfit(int[] prices) {
int[][] dp = new int[prices.length][2];
dp[0][0] = -prices[0]; // 第0天买入才能持有
dp[0][1] = 0; // 第0天没买,不持有,0元
for (int i = 1; i < prices.length; i++){
// 获取当天持有状态下的最高剩余金额
dp[i][0] = Math.max(dp[i-1][0], -prices[i]);
// 获取当天不持有状态下的最高剩余金额
dp[i][1] = Math.max(dp[i-1][1], dp[i-1][0] + prices[i]);
}
return dp[prices.length - 1][1]; // 最后一天肯定不持有的状态下是最大利润
}
}
- 时间复杂度:O(n),for循环遍历一次
- 空间复杂度:O(n),额外的二维数组
方法四 动态规划(两个变量滚动 / 一维数组滚动)
思路:模仿背包问题进行滚动,降低空间复杂度,但是要注意两个状态的更新顺序
- 当天不持有状态的最高剩余金额与昨天的两个状态都有关,先更新,覆盖昨天的不持有状态对今天的持有状态不影响
- 当天持有状态下的最高剩余金额只与昨天的持有状态有关,后更新,否则覆盖昨天的持有状态
class Solution {
public int maxProfit(int[] prices) {
int keep = -prices[0];
int notKeep = 0;
for (int i = 1; i < prices.length; i++){
// 获取当天不持有状态下的最高剩余金额(与昨天的两个状态都有关,先更新,覆盖昨天的不持有状态对今天的持有状态不影响)
notKeep = Math.max(notKeep, keep + prices[i]);
// 获取当天持有状态下的最高剩余金额(只与昨天的持有状态有关,后更新,否则覆盖昨天的持有状态)
keep = Math.max(keep, -prices[i]);
}
return notKeep; // 最后一天肯定不持有的状态下是最大利润
}
}
- 时间复杂度:O(n),for循环遍历一次
- 空间复杂度:O(1),只用了两个变量存储
【122. 买卖股票的最佳时机 II】中等题
方法一 贪心(很好理解,简单)
思路:只要当天比昨天的股价高,那么就能赚差价,叠加所有差价就是最大利润
class Solution {
public int maxProfit(int[] prices) {
int res = 0;
for (int i = 1; i < prices.length; i++) res += Math.max(prices[i] - prices[i-1], 0);
return res;
}
}
- 时间复杂度:O(n),for循环遍历一次
- 空间复杂度:O(1)
方法二 动态规划(二维数组)
思路:与121题一致,区别在于当天买入的时候,需要在昨天不持有的状态下购买。
即dp[i][0] = dp[i-1][1] - prices[i]
class Solution {
public int maxProfit(int[] prices) {
int[][] dp = new int[prices.length][2];
dp[0][0] = - prices[0];
dp[0][1] = 0;
for (int i = 1; i < prices.length; i++){
// 获取当天持有状态下的最高剩余金额
dp[i][0] = Math.max(dp[i-1][0], dp[i-1][1] - prices[i]); // 当天买的前提是昨天不持有
// 获取当天不持有状态下的最高剩余金额
dp[i][1] = Math.max(dp[i-1][1], dp[i-1][0] + prices[i]); // 当天卖的前提是昨天持有
}
return dp[prices.length - 1][1];
}
}
- 时间复杂度:O(n),for循环遍历一次
- 空间复杂度:O(n),额外的二维数组
方法三 动态规划(两个变量滚动 / 一维数组滚动)
思路:与121题思路基本一致,但是区别在于当天持有与不持有的最高剩余金额 都与昨天的两个状态有关,所以在滚动时, 需要防止当天的值覆盖昨天的值, 利用额外的变量记录先改变的状态即可。
class Solution {
public int maxProfit(int[] prices) {
int keep = -prices[0];
int notKeep = 0;
for (int i = 1; i < prices.length; i++){
// 先更新的是当天持有的状态,把昨天持有的状态先记录下来(用于更新当天的不持有状态)
int keepRaw = keep;
keep = Math.max(keep, notKeep - prices[i]);
notKeep = Math.max(notKeep, keepRaw + prices[i]);
}
return notKeep;
}
}
- 时间复杂度:O(n),for循环遍历一次
- 空间复杂度:O(1)
【123. 买卖股票的最佳时机 III】困难题
方法一 动态规划 (二维数组)
思路:最多可以交易两次,而且不能同时进行多次交易,即不能同时持有两只股,在再次购买前出售掉之前买入的股票,所以一共4个状态,需要用二维数组记录状态值。
1、状态0:第一次(持有)
- 当天之前就持有,保持昨天的持有状态,即dp[i][0] = dp[i-1][0]
- 当天才进行第一次购买,购买前剩余金额肯定为0,即dp[i][0] = 0 - prices[i] = - prices[i]
2、状态1:第一次(不持有)
- 之前都没买过,dp[i][1] = 0(后面两个状态>=0,这个状态可以忽略)
- 当天之前就售出,保持昨天的不持有状态,dp[i][1] = dp[i-1][1]
- 当天才进行第一次售出,昨天已经处于第一次持有状态 dp[i][1] = dp[i-1][0] + prices[i]
3、状态2:第二次(持有)
- 当天之前就持有,保持昨天的持有状态,dp[i][2] = dp[i-1][2]
- 当天才进行第二次购买,昨天已经处于第一次不持有状态,dp[i][2] = dp[i-1][1] - prices[i]
4、状态3:第二次(不持有)
- 当天之前就售出,保持昨天的不持有状态,dp[i][3] = dp[i-1][3]
- 当天才进行第二次售出,昨天已经处于第二次持有状态,dp[i][3] = dp[i-1][2] + prices[i]
class Solution {
public int maxProfit(int[] prices) {
// 4种状态:第一次(持有)、第一次(不持有)、第二次(持有)、第二次(不持有)
int dp[][] = new int[prices.length][4];
// 初始值
dp[0][0] = -prices[0]; // 第一次购买
dp[0][1] = 0;
dp[0][2] = -prices[0]; // 第二次购买
dp[0][3] = 0;
for (int i = 1; i < prices.length; i++){
// 第一次(持有)
dp[i][0] = Math.max(dp[i-1][0], 0 - prices[i]); // 注意当天进行第一次购买前,剩余金额肯定为0
// 第一次(不持有)
dp[i][1] = Math.max(dp[i-1][1], dp[i-1][0] + prices[i]); // 当天进行第一次售出,昨天肯定已经处于第一次持有状态
// 第二次(持有)
dp[i][2] = Math.max(dp[i-1][2], dp[i-1][1] - prices[i]); // 当天进行第二次购买,昨天肯定已经处于第一次不持有状态
// 第二次(不持有)
dp[i][3] = Math.max(dp[i-1][3], dp[i-1][2] + prices[i]); // 当天进行第二次售出,昨天肯定已经处于第二次持有状态
}
return dp[prices.length-1][3];
}
}
- 时间复杂度:O(n),for循环遍历一次
- 空间复杂度:O(n),二维数组第二个维度固定,第一个维度与输入数组的长度有关
方法二 动态规划(一维滚动数组)
思路:当天的状态3与昨天的状态2有关,当天的状态2与昨天的状态1有关,当天的状态1与昨天的状态0有关,当天的状态0也与昨天的状态0有关。所以需要倒序更新当天的状态,防止当天状态覆盖昨天的状态。
class Solution {
public int maxProfit(int[] prices) {
// 4种状态:第一次(持有)、第一次(不持有)、第二次(持有)、第二次(不持有)
int dp[] = new int[4];
// 初始值
dp[0] = -prices[0]; // 第一次购买
dp[1] = 0;
dp[2] = -prices[0]; // 第二次购买
dp[3] = 0;
for (int i = 1; i < prices.length; i++){
dp[3] = Math.max(dp[3], dp[2] + prices[i]);
dp[2] = Math.max(dp[2], dp[1] - prices[i]);
dp[1] = Math.max(dp[1], dp[0] + prices[i]);
dp[0] = Math.max(dp[0], 0 - prices[i]);
}
return dp[3];
}
}
- 时间复杂度:O(n),for循环遍历一次
- 空间复杂度:O(1),一维数组长度固定为4
【188. 买卖股票的最佳时机 IV】困难题
思路:参考123题的方法二,本题与132题的区别在于,最多交易的次数不固定,由输入参数k指定,那么就要寻找初始化和更新状态的规律。
初始化规律:所有持有状态下(状态0、状态2、...)的初始值都是 - prices[0]
更新状态规律:当天后面的状态与昨天前面的状态有关,需要倒序更新所有状态,防止更新后的新状态覆盖昨天的状态,注意dp[0]要单独处理。
class Solution {
public int maxProfit(int k, int[] prices) {
// 注意:交易k次,有k*2个状态
int[] dp = new int[k*2];
// 初始化
for (int i = 0; i < k*2; i++){
if (i % 2 == 0) dp[i] = -prices[0];
}
// 滚动更新每一天的所有状态
for (int i = 1; i < prices.length; i++){
// 倒序更新所有状态
for (int j = k*2-1; j > 0; j--){
int tag = j % 2 == 1 ? 1 : -1;
dp[j] = Math.max(dp[j], dp[j-1] + tag * prices[i]);
}
// 只有dp[0]更新需要特殊单独处理
dp[0] = Math.max(dp[0], -prices[i]);
}
return dp[k*2-1];
}
}
- 时间复杂度:O(n),prices数组的长度
- 空间复杂度:O(k),dp数组的长度为2k
【309. 买卖股票的最佳时机含冷冻期】中等题
方法一 动态规划(二维数组)
思路:含冷冻期,即不持有状态要分为【处于冷冻期】和【不处于冷冻期】,加上持有状态,一共3种状态(所有状态都是指交易时间结束之后的状态)。取最后一天的非持有状态下(即状态2和状态3)的剩余金额的最大值即题目所求的最大利润。
步骤:
1、确定各个状态的含义
- 状态0:今天处于持有状态,可以是今天之前就持有,也可以是今天买入才持有
- 状态1:今天处于不持有状态,并且处于冷冻期,即只能今天卖出,那昨天要持有。
- 状态2:今天处于不持有状态,并且处于非冷冻期,可以是昨天就处于非冷冻期,也可以是昨天处于冷冻期。
2、确定递推关系
- dp[i][0] = Math.max(dp[i-1][0], dp[i-1][2] - prices[i])
- 今天之前就持有:dp[i][0] = dp[i-1][0]
- 今天买入才持有(昨天非冷冻期):dp[i][0] = dp[i-1][2] - prices[i]
- dp[i][1] = dp[i-1][0] + prices[i]
- 只能今天卖出(昨天持有):dp[i-1][0] + prices[i]
- dp[i][2] = Math.max(dp[i-1][2], dp[i-1][1])
- 昨天处于非冷冻期:dp[i][2] = dp[i-1][2]
- 昨天处于冷冻期:dp[i][2] = dp[i-1][1]
3、确定初始值
dp[0][0] = -prices[0]:第0天处于持有状态,只能是第0天买入了
dp[0][1] = 0, dp[0][2] = 0:不存在第0天之前买入,所以不能卖出,金额为0
class Solution {
public int maxProfit(int[] prices) {
int[][] dp = new int[prices.length][3];
// 初始化
dp[0][0] = -prices[0]; // 持有状态
dp[0][1] = 0; // 不持有状态(冷冻期)
dp[0][2] = 0; // 不持有状态(非冷冻期)
// 递推关系
for (int i = 1; i < prices.length; i++){
dp[i][0] = Math.max(dp[i-1][0], dp[i-1][2] - prices[i]); // 今天之前持有或今天买入才持有
dp[i][1] = dp[i-1][0] + prices[i]; // 只能今天卖出,那必须昨天持有,交易时间结束后处于冷冻期
dp[i][2] = Math.max(dp[i-1][2], dp[i-1][1]); // 昨天非冷冻期或冷冻期,今天都是非冷冻期
}
return Math.max(dp[prices.length-1][1], dp[prices.length-1][2]); // 最后一天的最大利润一定出现在不持有状态中的一种
}
}
- 时间复杂度:O(n),for循环遍历一次
- 空间复杂度:O(n),二维数组第二个维度固定为3,第一个维度与输入数组的长度有关
注意:
1、昨天卖出股票后,交易时间过后,才处于冷冻期,那么今天交易时间之前仍是冷冻期,交易时间之后就处于非冷冻期,这样刚好冷冻了一天。2、对于持有状态,今天买入只能是昨天处于非冷冻期(状态2),因为昨天处于冷冻期,则交易时间结束之前还是处于冷冻期,无法买入。
方法二 动态规划(一维滚动数组)
思路:优化空间复杂度O(n) -> O(1),由方法一的递推关系可以看出,
今天的状态0:与昨天的状态0和昨天的状态2有关
今天的状态1:只与昨天的状态0有关
今天的状态2:与昨天的状态2和昨天的状态1有关
由于三个状态之间相互依赖,只能用一个额外的变量记住昨天状态1的值,然后先更新今天的状态1,再更新状态0,最后更新状态2。
class Solution {
public int maxProfit(int[] prices) {
int[] dp = new int[3];
// 初始化
dp[0] = -prices[0]; // 持有状态
dp[1] = 0; // 不持有状态(冷冻期)
dp[2] = 0; // 不持有状态(非冷冻期)
// 递推关系
for (int i = 1; i < prices.length; i++){
int tmp = dp[1];
dp[1] = dp[0] + prices[i]; // dp[0]是昨天的状态0
dp[0] = Math.max(dp[0], dp[2] - prices[i]); // dp[0]和dp[2]都是昨天的状态
dp[2] = Math.max(dp[2], tmp); // 现在的dp[1]是今天的状态1,tmp才是昨天的状态1
}
return Math.max(dp[1], dp[2]); // 最后一天的最大利润一定出现在不持有状态中的一种
}
}
- 时间复杂度:O(n),for循环遍历一次
- 空间复杂度:O(1),一维数组长度固定为3,额外的变量始终只有1个,与输入数组长度无关
【714. 买卖股票的最佳时机含手续费】中等题
思路:与122题基本一致,都是无限次交易次数,但区别在于含手续费,即今天卖出时,剩余金额还需要减去手续费。
class Solution {
public int maxProfit(int[] prices, int fee) {
int stat0 = -prices[0];
int stat1 = 0;
for (int i = 1; i < prices.length; i++){
int tmp = stat0;
stat0 = Math.max(stat0, stat1 - prices[i]);
stat1 = Math.max(stat1, tmp + prices[i] - fee);
}
return stat1;
}
}
- 时间复杂度:O(n),for循环遍历一次
- 空间复杂度:O(1),只使用了三个额外的变量