难度简单
给定一个数组,它的第 i 个元素是一支给定股票第 i 天的价格。
设计一个算法来计算你所能获取的最大利润。你可以尽可能地完成更多的交易(多次买卖一支股票)。
**注意:**你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。
示例 1:
输入: [7,1,5,3,6,4]
输出: 7
解释: 在第 2 天(股票价格 = 1)的时候买入,在第 3 天(股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5-1 = 4 。
随后,在第 4 天(股票价格 = 3)的时候买入,在第 5 天(股票价格 = 6)的时候卖出, 这笔交易所能获得利润 = 6-3 = 3 。
示例 2:
输入: [1,2,3,4,5]
输出: 4
解释: 在第 1 天(股票价格 = 1)的时候买入,在第 5 天 (股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5-1 = 4 。
注意你不能在第 1 天和第 2 天接连购买股票,之后再将它们卖出。
因为这样属于同时参与了多笔交易,你必须在再次购买前出售掉之前的股票。
示例 3:
输入: [7,6,4,3,1]
输出: 0
解释: 在这种情况下, 没有交易完成, 所以最大利润为 0。
提示:
1 <= prices.length <= 3 * 10 ^ 4
0 <= prices[i] <= 10 ^ 4
我是最后写搜索算法的,但是把它放在了开头,意思是想告诉自己:搜索算法应该是最先想到的。
可能暴力搜索不能让有些面试官满意,但是我觉得有些不拘一格面试官会认可你先说出搜索的算法这个思路,然后再以搜索算法为起点,考虑更好的算法。
根据题意:因为不限制交易次数,在每一天,我就可以根据当前是否持有股票选择相应的操作。“暴力搜索” 也叫 “回溯搜索”、“回溯法”,首先画出树形图。
下面编写代码:
参考代码 1:
public class Solution {
private int res;
public int maxProfit(int[] prices) {
int len = prices.length;
if (len < 2) {
return 0;
}
this.res = 0;
dfs(prices, 0, len, 0, res);
return this.res;
}
/**
* @param prices 股价数组
* @param index 当前是第几天,从 0 开始
* @param status 0 表示不持有股票,1表示持有股票,
* @param profit 当前收益
*/
private void dfs(int[] prices, int index, int len, int status, int profit) {
if (index == len) {
this.res = Math.max(this.res, profit);
return;
}
dfs(prices, index + 1, len, status, profit);
if (status == 0) {
// 可以尝试转向 1
dfs(prices, index + 1, len, 1, profit - prices[index]);
} else {
// 此时 status == 1,可以尝试转向 0
dfs(prices, index + 1, len, 0, profit + prices[index]);
}
}
}
class Solution {
public int maxProfit(int[] prices) {
return calculate(prices, 0);
}
public int calculate(int prices[], int s) {
if (s >= prices.length)
return 0;
int max = 0;
for (int start = s; start < prices.length; start++) {
int maxprofit = 0;
for (int i = start + 1; i < prices.length; i++) {
if (prices[start] < prices[i]) {
int profit = calculate(prices, i + 1) + prices[i] - prices[start];
if (profit > maxprofit)
maxprofit = profit;
}
}
if (maxprofit > max)
max = maxprofit;
}
return max;
}
}
复杂度分析
很显然,超时在意料之中。注意看这个超时是在数据规模很大的时候,因此,可以说明搜索算法是正确的。
事实上,写完搜索算法,状态转移基本上就已经能够思考清楚了,不过我们还是先来看看贪心算法吧。
这道题使用贪心算法的流程是这样的:
i
天(这里 i >= 1
)开始,与第 i - 1
的股价进行比较,如果股价有上升(严格上升),就将升高的股价( prices[i] - prices[i- 1]
)记入总利润,按照这种算法,得到的结果就是符合题意的最大利润。下面对这个算法进行几点说明:
1、该算法仅可以用于计算,但计算的过程并不是真正交易的过程,但可以用贪心算法计算题目要求的最大利润。下面说明这个等价性:以 [1, 2, 3, 4]
为例,这 4
天的股价依次上升,按照贪心算法,得到的最大利润是:
res = (prices[3] - prices[2]) + (prices[2] - prices[1]) + (prices[1] - prices[0])
= prices[3] - prices[0]
仔细观察上面的式子,按照贪心算法,在索引为 1
、2
、3
的这三天,我们做的操作应该是买进昨天的,卖出今天的,虽然这种操作题目并不允许,但是它等价于:“在索引为 0
的那一天买入,在索引为 3
的那一天卖出”。
2、解释一下它为什么叫 “贪心算法”
我们还是回到贪心算法的定义:(下面是来自《算法导论(第三版)》第 16 章的叙述)
“贪心算法” 在每一步总是做出在当前看来最好的选择。
因此,
0
(3)负数。贪心算法的决策是:只加正数。
参考代码 2:
public class Solution {
public int maxProfit(int[] prices) {
int res = 0;
int len = prices.length;
for (int i = 0; i < len - 1; i++) {
int diff = prices[i + 1] - prices[i];
if (diff > 0) {
res += diff;
}
}
return res;
}
}
复杂度分析:
等价写法:
参考代码 3:
Java 代码:
public class Solution {
public int maxProfit(int[] prices) {
int res = 0;
int len = prices.length;
for (int i = 0; i < len - 1; i++) {
res += Math.max(prices[i + 1] - prices[i], 0);
}
return res;
}
}
复杂度分析:(同上)
下面证明 “贪心算法” 的有效性。
我们借助 “差分” 这个概念,可以证明 “贪心算法” 的有效性。贪心算法就是选择那些所有差分(严格)大于 0
的数,把它们相加即可。
使用反证法:假设 “贪心算法” 得到的解并不是最优解,即我们还能够找到一个可行解比 “贪心算法” 得到的利润还多。差分数组中除了差分为正数的项以外,还有就是差分为 0
的项与差分为负数的项。“贪心算法” 是所有差分为正数的项的和。
1、如果可行解在 “贪心算法” 的基础上,选择了差分为 0
的项,得到的结果与“贪心算法”得到的结果一样,因此加上差分为 0
的项不会比“贪心算法”得到的结果更好;
2、如果可行解在 “贪心算法” 的基础上,选择了差分为负数的项,加上一个负数得到的结果一定比 “贪心算法” 得到的结果要少,加上差分为负数的项,一定比 “贪心算法” 得到的结果更少;
3、如果可行解在 “贪心算法” 的基础上,去掉了任何一个差分为正数的项,同上,得到的结果一定比 “贪心算法” 得到的结果要小,因此,“贪心算法” 的所有组成项不能删去任何一个。
综上,除了 “贪心算法” 以外,找不到一个更优的解法,因此 “贪心算法” 就是最优解。(证完)
想到动态规划的原因是:可以用贪心算法解决的问题,一般情况下都可以用动态规划。因此,不妨从 “状态”、“状态转移方程” 的角度考虑一下,使用动态规划的思路解决这道问题。
根据 「力扣」第 121 题的思路,需要设置一个二维矩阵表示状态。
状态 dp[i][j]
定义如下
i
表示索引为 i
的那一天(具有前缀性质,即考虑了之前天数的收益)能获得的最大利润;j
表示索引为 i
的那一天是持有股票,还是持有现金。这里 0
表示持有现金(cash),1
表示持有股票(stock)。(状态转移方程写在代码中)
说明:
0
的时候的值即可。起始的时候:
dp[0][0] = 0
;dp[0][1] = -prices[i]
;终止的时候,上面也分析了,输出 dp[len - 1][0]
,因为一定有 dp[len - 1][0] > dp[len - 1][1]
。
参考代码 4:
public class Solution {
public int maxProfit(int[] prices) {
int len = prices.length;
if (len < 2) {
return 0;
}
// 0:持有现金
// 1:持有股票
// 状态转移:0 → 1 → 0 → 1 → 0 → 1 → 0
int[][] dp = new int[len][2];
dp[0][0] = 0;
dp[0][1] = -prices[0];
for (int i = 1; i < len; 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[len - 1][0];
}
}
复杂度分析:
我们也可以将状态数组分开设置,语义更明确。
参考代码 5:
public class Solution {
public int maxProfit(int[] prices) {
int len = prices.length;
if (len < 2) {
return 0;
}
// cash:持有现金
// hold:持有股票
// 状态数组
// 状态转移:cash → hold → cash → hold → cash → hold → cash
int[] cash = new int[len];
int[] hold = new int[len];
cash[0] = 0;
hold[0] = -prices[0];
for (int i = 1; i < len; i++) {
// 这两行调换顺序也是可以的
cash[i] = Math.max(cash[i - 1], hold[i - 1] + prices[i]);
hold[i] = Math.max(hold[i - 1], cash[i - 1] - prices[i]);
}
return cash[len - 1];
}
}
复杂度分析:(同上)
由于当前行只参考上一行,每一行就 2 个值,因此可以考虑使用“滚动变量”(“滚动数组”技巧)。
参考代码 6:
public class Solution {
public int maxProfit(int[] prices) {
int len = prices.length;
if (len < 2) {
return 0;
}
// cash:持有现金
// hold:持有股票
// 状态转移:cash → hold → cash → hold → cash → hold → cash
int cash = 0;
int hold = -prices[0];
int preCash = cash;
int preHold = hold;
for (int i = 1; i < len; i++) {
cash = Math.max(preCash, preHold + prices[i]);
hold = Math.max(preHold, preCash - prices[i]);
preCash = cash;
preHold = hold;
}
return cash;
}
}
复杂度分析:
算法
假设给定的数组为:
[7, 1, 5, 3, 6, 4]
如果我们在图表上绘制给定数组中的数字,我们将会得到:
如果我们分析图表,那么我们的兴趣点是连续的峰和谷。
用数学语言描述为:
T o t a l P r o f i t = ∑ i ( h e i g h t ( p e a k i ) − h e i g h t ( v a l l e y i ) ) Total Profit= \sum_{i}(height(peak_i)-height(valley_i)) TotalProfit=∑i(height(peaki)−height(valleyi))
关键是我们需要考虑到紧跟谷的每一个峰值以最大化利润。如果我们试图跳过其中一个峰值来获取更多利润,那么我们最终将失去其中一笔交易中获得的利润,从而导致总利润的降低。
例如,在上述情况下,如果我们跳过 p e a k i peak_i peaki和 v a l l e y j valley_j valleyj试图通过考虑差异较大的点以获取更多的利润,获得的净利润总是会小与包含它们而获得的净利润,因为 C总是小于 A+B。
class Solution {
public int maxProfit(int[] prices) {
int i = 0;
int valley = prices[0];
int peak = prices[0];
int maxprofit = 0;
while (i < prices.length - 1) {
while (i < prices.length - 1 && prices[i] >= prices[i + 1])
i++;
valley = prices[i];
while (i < prices.length - 1 && prices[i] <= prices[i + 1])
i++;
peak = prices[i];
maxprofit += peak - valley;
}
return maxprofit;
}
}
复杂度分析