想要精通算法和SQL的成长之路 - 买卖股票的最佳时机系列

前言

想要精通算法和SQL的成长之路 - 系列导航

一. 买卖股票的最佳时机(买卖一次)

原题链接
我们先来看下最简单的股票问题。给定一个数组 prices ,它的第 i 个元素 prices[i] 表示一支给定股票第 i 天的价格。

你只能选择某一天买入这只股票,并选择在未来的某一个不同的日子卖出该股票。设计一个算法来计算你所能获取的最大利润。

  • 输入:[7,1,5,3,6,4]
  • 输出:5
  • 解释:在第 2 天(股票价格 = 1)的时候买入,在第 5 天(股票价格 = 6)的时候卖出,最大利润 = 6-1 = 5 。

我们从题目所给信息可以得到;

  1. 股票只能买一次和卖一次。
  2. 那么当我们买入了一只股票,它的最大利润就是在历史最低点卖出。

那么就可以用贪心的思想,在遍历股票价格的同时。我们遍历到一个价格,就假设当天把他卖出,同时维护历史最低价,然后计算差值就是最大利润。代码如下:

public int maxProfit(int[] prices) {
    int minPrice = Integer.MAX_VALUE;
    int res = 0;
    for (int price : prices) {
        // 找到前几天的最小值
        minPrice = Math.min(minPrice, price);
        // 我们求得:如果卖当前股票,那么当前股票的 最大利润 = 当前价格 - 历史最小价
        res = Math.max(res, price - minPrice);
    }
    return res;
}

二. 买卖股票的最佳时机II(买卖多次)

原题链接
在每一天,你可以决定是否购买和/或出售股票。你在任何时候最多只能持有 一 股票。你也可以先购买,然后在同一天出售。也就是在第一题的基础上,可以选择买卖多次。

其实这样反而比第一题要简单:

  • 既然每天都可以买卖的话,那么我们只要保证,卖的时候一定是涨的。 价格下降的日子都不卖。
  • 也就是说,我们可以无视价格下降的日子

代码如下:

public int maxProfit(int[] prices) {
    int res = 0;
    for (int i = 0; i < prices.length - 1; i++) {
        int cost = prices[i + 1] - prices[i];
        // 只有涨的价格,才有利润。如果是亏的,不计入
        if (cost > 0) {
            res += cost;
        }
    }
    return res;
}

三. 买卖股票的最佳时机III(最多两次)

原题链接

在第二题的基础上,改变了限制:股票最多只能买两次。

这题目可就不能跟上面两道题一样了,我们需要开始用变量代表各个状态来完成价格的计算了。也就是动态规划。这里面用到的状态有三种:

  • 当前是第几天。
  • 当前是否持股:持股/为持股。
  • 卖出过几次:0次,1次,2次。

那么我们用三维数组来表达:dp[i][j][r],代表当前天数的最大利润。

  • i:第几天。
  • j:可能的值:0,1。0代表未持股,1代表持股。
  • r:可能的值:0,1,2。卖出过几次。

那么就有

int len = prices.length;
int[][][] dp = new int[len][2][3];
// 第一天,未持股,没卖过
dp[0][0][0] = 0;
// 第一天,买入一只股票,没卖过。
dp[0][1][0] = -prices[0];
// 第一天不可能卖过,因此凡是r = 1或者2的全部是最小值。
dp[0][0][1] = dp[0][0][2] = dp[0][1][1] = dp[0][1][2] = min;

那么接下来需要开始分析各个状态情况下公式的推导:

状态利润
今日未持股,卖出过0次 dp[i][0][0]都没买过,利润自然是0,无论哪一天: dp[i][0][0] = 0
今日未持股,卖出过1次 dp[i][0][1]dp[i][0][1] = Max(dp[i-1][1][0]+price[i](今日卖出),dp[i-1][0][1](昨日未持股))
今日未持股,卖出过2次 dp[i][0][2]dp[i][0][2] = Max(dp[i-1][1][1]+price[i](今日卖出),dp[i-1][0][2](昨日未持股))
持股,卖出过0次 dp[i][1][0]dp[i][1][0] = Max(dp[i-1][0][0] - price[i](昨日买入了),dp[i-1][1][0](前几天就买过了))
持股,卖出过1次 dp[i][1][1]dp[i][1][1] = Max(dp[i-1][0][1] - price[i](昨日买入了),dp[i-1][1][1](前几天就买过了))
持股,卖出过2次 dp[i][1][2]最多卖两次,不能再卖下去,这种情况不存在:dp[i][1][02 = Integer.MIN

那么最后股票的最大利润就有两种情况:

  • 卖出过1次,未持股:dp[len-1][0][1]
  • 卖出过2次,未持股:dp[len-1][0][2]

那么最终代码如下:

public int maxProfit(int[] prices) {
    int[][][] dp = new int[prices.length][2][3];
    int min = Integer.MIN_VALUE / 2;
    // 第一天,未持股,没卖过
    dp[0][0][0] = 0;
    // 第一天,买入一只股票,没卖过。
    dp[0][1][0] = -prices[0];
    // 第一天不可能卖过,因此凡是r = 1或者2的全部是最小值。
    dp[0][0][1] = dp[0][0][2] = dp[0][1][1] = dp[0][1][2] = min;
    // 第一天相关的值已经初始化了,从第二天开始计算
    for (int i = 1; i < prices.length; i++) {
        // 套公式 ,当前未持股
        dp[i][0][0] = 0;
        dp[i][0][1] = Math.max(dp[i - 1][1][0] + prices[i], dp[i - 1][0][1]);
        dp[i][0][2] = Math.max(dp[i - 1][1][1] + prices[i], dp[i - 1][0][2]);
		// 持股
        dp[i][1][0] = Math.max(dp[i - 1][0][0] - prices[i], dp[i - 1][1][0]);
        dp[i][1][1] = Math.max(dp[i - 1][0][1] - prices[i], dp[i - 1][1][1]);
        dp[i][1][2] = min;
    }
    return Math.max(0, Math.max(dp[prices.length - 1][0][1], dp[prices.length - 1][0][2]));
}

四. 买卖股票的最佳时机IV(最多k次)

原题链接

这题的难度其实并没有上升太多,因为在第三题里面,已经把状态的转移公式写的很清楚了。只不过是买卖的次数由2次变更为k次罢了。

那么代码的不同无非就是两个:

  • 初始化dp数组的时候,2需要改成k+1
  • for循环中,需要在增加一个for循环。用来遍历k的下标数组。因为第三题中,k只有0,1两种可能,因此没写成for循环。

其中的公式更改为下

// 如果是持股         昨天买入(因此昨天未持股)  前几天就持股了
dp[i][1][k] = max(dp[i-1][0][k] - price , dp[i-1][1][k])
// 如果是未持股,      昨天卖出(因此昨天持股) 。 前几天也未持股
dp[i][0][k] = max(dp[i-1][1][k-1] + price , dp[i-1][0][k])

代码如下:

// 第一天相关的值已经初始化了,从第二天开始计算
for (int i = 1; i < prices.length; i++) {
    for (int k = 1; k <= K; k++) {
        // 如果是持股         昨天买入(因此昨天未持股)  前几天就持股了
        dp[i][1][k] = Math.max(dp[i - 1][0][k] - prices[i], dp[i - 1][1][k]);
        // 如果是未持股,      昨天卖出(因此昨天持股) 。 前几天也未持股
        dp[i][0][k] = Math.max(dp[i - 1][1][k - 1] + prices[i], dp[i - 1][0][k]);
    }
}

那么初始化相关的代码我们就要做(遍历r位):

  • 第一天,如果持股,那么无论k等于多少,都赋值为-prices[0]
  • 第一天,如果不持股,那么最大利润就是0。
for (int k = 0; k <= K; k++) {
    dp[0][0][k] = 0;// 第一天不持股,k不重要
    dp[0][1][k] = -prices[0];// 第一天持股
}

如果是后面几天(遍历i位):

  • 没持股、没卖过,就等于没买过。最大利润为0。
  • 持股,没卖过。那么当前的值取 Max(昨天买入,前几天就持有)的最大值。
for (int i = 1; i < prices.length; i++) {
    dp[i][0][0] = 0;
    dp[i][1][0] = Math.max(dp[i - 1][0][0] - prices[i], dp[i - 1][1][0]);
}

最终代码就是:

public int maxProfit(int K, int[] prices) {
    int[][][] dp = new int[prices.length][2][K + 1];
    for (int k = 0; k <= K; k++) {
        dp[0][0][k] = 0;// 第一天不持股,k不重要
        dp[0][1][k] = -prices[0];// 第一天持股
    }
    // 第i天如果买入股票,没卖过,未持股
    for (int i = 1; i < prices.length; i++) {
        dp[i][0][0] = 0;
        dp[i][1][0] = Math.max(dp[i - 1][0][0] - prices[i], dp[i - 1][1][0]);
    }
    // 第一天相关的值已经初始化了,从第二天开始计算
    for (int i = 1; i < prices.length; i++) {
        for (int k = 1; k <= K; k++) {
            // 如果是持股         昨天买入(因此昨天未持股)  前几天就持股了
            dp[i][1][k] = Math.max(dp[i - 1][0][k] - prices[i], dp[i - 1][1][k]);
            // 如果是未持股,      昨天卖出(因此昨天持股) 。 前几天也未持股
            dp[i][0][k] = Math.max(dp[i - 1][1][k - 1] + prices[i], dp[i - 1][0][k]);
        }
    }
    return Math.max(0, Math.max(dp[prices.length - 1][0][K], dp[prices.length - 1][0][K - 1]));
}

五. 最佳买卖股票时机含冷冻期

原题链接
总结就是:

  • 可以多买多卖。
  • 卖了之后的那一天不能买,即1天冷冻期。

如果有冷冻期,那么我们可以把股票的持有状态分为以下三种:我们用dp[i][j]来代表第i天的最大利润。j代表对应的状态。

  • 持有股票。dp[i][0]
  • 未持有股票,并且处于冷冻期dp[i][1]
  • 未持有股票,处于非冷冻期dp[i][2]

先说第一种,持有股票的状态怎么来:

  • 第一种:昨日刚买的股票,此时处于非冷冻期(冷冻期不可买)
  • 第二种:前几天就持有了。
dp[i][0] = Math.max(dp[i-1][2] - price[i] , dp[i-1][0]);

再说下未持有+冷冻期的状态怎么来,只有一种情况:

  • 就是昨天刚卖的。
dp[i][1] = dp[i-1][0] + price[i]

未持有+ 非冷冻期的状态:

  • 第一种:昨天未持有+非冷冻期。
  • 第二种:昨天未持有+冷冻期。
dp[i][2] = Math.max(dp[i-1][1] , dp[i-1][2]);

初始化即第一天买入:

dp[0][0] = -price[0]

结果:

public int maxProfit2(int[] prices) {
    int len = prices.length;
    // 0:持股。1:未持股+冷冻。2:未持股+非冷冻
    int[][] dp = new int[len][3];
    // 持股
    dp[0][0] = -prices[0];
    for (int i = 1; i < len; i++) {
        dp[i][0] = Math.max(dp[i - 1][2] - prices[i], dp[i - 1][0]);
        dp[i][1] = dp[i - 1][0] + prices[i];
        dp[i][2] = Math.max(dp[i - 1][1], dp[i - 1][2]);
    }
    // 最终肯定是未持股的状态,可能处于冷冻期,也可能处于非冷冻期
    return Math.max(dp[len - 1][1], dp[len - 1][2]);
}

六. 买卖股票的最佳时机含手续费

原题链接

给定一个整数数组 prices,其中 prices[i]表示第 i 天的股票价格 ;整数 fee 代表了交易股票的手续费用。

你可以无限次地完成交易,但是你每笔交易都需要付手续费。如果你已经购买了一个股票,在卖出它之前你就不能再继续购买股票了。返回获得利润的最大值。

  • 输入:prices = [1, 3, 2, 8, 4, 9], fee = 2
  • 输出:8
  • 解释:能够达到的最大利润:

在此处买入 prices[0] = 1
在此处卖出 prices[3] = 8
在此处买入 prices[4] = 4
在此处卖出 prices[5] = 9
总利润: ((8 - 1) - 2) + ((9 - 4) - 2) = 8

这个题目我们可以定义:

  • dp[i][0]:第i天未持有股票的最大利润。
  • dp[i][1]:第i天持有股票的最大利润。

那么推导公式如下:

// 今日不持有 = max (昨日卖出 ,和昨日不持有)
dp[i][0] = Math.max(dp[i-1][1]+prices[i]-fee,dp[i-1][0]);
// 今日持有 = max (昨日买入 ,和昨日持有)
dp[i][1] = Math.max(dp[i-1][0]-prices[i],dp[i-1][1]);

初始值:

  • 第一天不买入: dp[0][0] = 0
  • 第一天买入:dp[0][1] = -prices[0]

最终结果:

public int maxProfit3(int[] prices, int fee) {
    int len = prices.length;
    int[][] dp = new int[len][2];
    dp[0][0] = 0;
    dp[0][1] = -prices[0];
    for (int i = 1; i < len; i++) {
        // 今日不持有 = max (昨日卖出 ,和昨日不持有)
        dp[i][0] = Math.max(dp[i - 1][1] + prices[i] - fee, dp[i - 1][0]);
        // 今日持有 = max (昨日买入 ,和昨日持有)
        dp[i][1] = Math.max(dp[i - 1][0] - prices[i], dp[i - 1][1]);
    }
    // 返回最终结果,一定是不持有状态
    return dp[len - 1][0];
}

七. 总结

  • 买卖股票(一次买卖):贪心算法。我们假设股票在卖出去的时候,永远都是历史最小价格收入的。
  • 买卖股票(多次买卖):一次遍历。只看涨的,不看降的。
  • 买卖股票(最多两次):三维数组,dp[i][j][r] :第i天,j代表是否持有:0,1。r代表卖过几次:0,1,2。
  • 买卖股票(最多k次):同样三维数组dp[i][j][r] :第i天,j代表是否持有:0,1。r代表卖过几次:0,1,2…k。
  • 买卖股票(含冷冻期):二维数组dp[i][j],第i天,j用三个数字代表三个状态。持有、未持有+冷冻期、未持有+非冷冻期
  • 买卖股票(含手续费):二维数组dp[i][j],第i天,j用两个数字代表两个状态:持有、未持有。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Zong_0915

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值