前言
通过卖股票最佳时机和卖股票最佳时机升级版,让思路一步一步的出来,解决一个复杂的问题。
一、案例
1、最佳时机
给定一个数组 prices ,它的第 i 个元素 prices[i] 表示一支给定股票第 i 天的价格。
你只能选择 某一天 买入这只股票,并选择在 未来的某一个不同的日子 卖出该股票。设计一个算法来计算你所能获取的最大利润。
返回你可以从这笔交易中获取的最大利润。如果你不能获取任何利润,返回 0 。
示例 1:
输入:[7,1,5,3,6,4]
输出:5
解释:在第 2 天(股票价格 = 1)的时候买入,在第 5 天(股票价格 = 6)的时候卖出,最大利润 = 6-1 = 5 。
注意利润不能是 7-1 = 6, 因为卖出价格需要大于买入价格;同时,你不能在买入前卖出股票。
示例 2:
输入:prices = [7,6,4,3,1]
输出:0
解释:在这种情况下, 没有交易完成, 所以最大利润为 0。
2、最佳时机II
给定一个数组 prices ,其中 prices[i] 表示股票第 i 天的价格。
在每一天,你可能会决定购买和/或出售股票。你在任何时候 最多 只能持有 一股 股票。你也可以购买它,然后在 同一天 出售。
返回 你能获得的 最大 利润 。
示例 1:
输入: prices = [7,1,5,3,6,4]
输出: 7
解释: 在第 2 天(股票价格 = 1)的时候买入,在第 3 天(股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5-1 = 4 。
随后,在第 4 天(股票价格 = 3)的时候买入,在第 5 天(股票价格 = 6)的时候卖出, 这笔交易所能获得利润 = 6-3 = 3 。
示例 2:
输入: prices = [1,2,3,4,5]
输出: 4
解释: 在第 1 天(股票价格 = 1)的时候买入,在第 5 天 (股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5-1 = 4 。
注意你不能在第 1 天和第 2 天接连购买股票,之后再将它们卖出。因为这样属于同时参与了多笔交易,你必须在再次购买前出售掉之前的股票。
示例 3:
输入: prices = [7,6,4,3,1]
输出: 0
解释: 在这种情况下, 没有交易完成, 所以最大利润为 0。
题解
1、最佳时机
最佳时机可以体现为买股票的最佳时机和卖股票的最佳时机,分别为买时定在最小价格和卖时的价格减去买时的最小是否有最大差值即利润。
所以从左到右遍历,记录最小价格,然后记录一个最大差值,每次遍历值减去最小价格得到利润值,最后与前面记录的最大差值即最大利润做比较,选出最大利润。
public int maxProfit(int[] prices) {
//往后遍历,当前数字减之前的最小值,来取最小值。
int res = 0;
int min = prices[0];
for (int price : prices) {
int gap = price - min;
res = res < gap ? gap : res;
min = min > price ? price : min;
}
return res;
}
2、最佳时机II
在寻找最佳时机I的基础,只需再进行分段求和,而分段的时机就在于 gap 是前一段利益的最大值时。
注,需要防止几段和都小于遍历当前值减去全局最小值,所以应该有一个全局最小值和一个从全局最小值开始的后段利益之和。
如 [1 2 4 5],(2 - 1)+(5 - 4)< 5 - 1;
package com.xhu.offer.tencent;
//最大股票利润,一个难题真的做不来,不能一下想到解决办法,真的是浪费时间,没有意义。
// 如果像这样不断加大难度,还能做出来,而且训练了思维,比较有意义。
//
public class MaxProfit {
public int maxProfit(int[] prices) {
//往后遍历,当前数字减之前的最小值,来取最小值。
int res = 0;
int min = prices[0];
for (int price : prices) {
int gap = price - min;
res = res < gap ? gap : res;
min = min > price ? price : min;
}
return res;
}
//卖股票的最佳时机II
//总结:分析完问题,需要归纳整理好问题,防止乱导致出错。
public int maxProfit2(int[] prices) {
//同样往后遍历,但是要每一段取了利润的同时,要和以前的最小值去比较,当前差距是否大于这一段的所有和。
int total_sum = 0;//总最大和
int cur_segment_sum = 0, cur_segment_min = prices[0];//将数组分为很多段数组,每段有其最小值和当段的最大和。
int context_min_sum = 0, context_min = prices[0];//整个数组有一个最小值,当前遍历值到前面最小值之间的几段总和。
for (int price : prices) {
int gap = price - cur_segment_min;
if (cur_segment_sum < gap) {
//一种更正方式,即不应该分段的时候。
if (price - context_min > context_min_sum + gap) {
//更正total_sum
total_sum -= context_min_sum;
total_sum += price - context_min;
//更正min_sum,min不用更正
context_min_sum = price - context_min;
} else {
//另一种方式更新total_sum和min_sum,即应该分段求和。
total_sum += gap;
context_min_sum += gap;
}
//开始下一段的最大利益化
cur_segment_sum = 0;
cur_segment_min = price;
} else {//此时说明前后产出利益为负,需更新当前段最小值及左缀最小值。
cur_segment_min = cur_segment_min > price ? price : cur_segment_min;
//更正min部分
if (context_min >= price) {
//更正min
context_min = price;
//更正min_sum
context_min_sum = 0;
}
}
}
return total_sum;
}
public int maxProfit3(int[] prices) {
//同样往后遍历,但是要每一段取了利润的同时,要和以前的最小值去比较,当前差距是否大于这一段的所有和。
int total_sum = 0;//总最大和
int cur_segment_min = prices[0];//将数组分为很多段数组,每段有其最小值和当段的最大和。
int context_min_sum = 0, context_min = prices[0];//整个数组有一个最小值,当前遍历值到前面最小值之间的几段总和。
for (int i = 0; i < prices.length; i++) {
if(i+1 < prices.length && prices[i] <= prices[i+1]) continue;
int gap = prices[i] - cur_segment_min;
//一种更正方式,即不应该分段的时候。
if (prices[i] - context_min > context_min_sum + gap) {
//更正total_sum
total_sum -= context_min_sum;
total_sum += prices[i] - context_min;
//更正min_sum,min不用更正
context_min_sum = prices[i] - context_min;
} else {
//另一种方式更新total_sum和min_sum,即应该分段求和。
total_sum += gap;
context_min_sum += gap;
}
if(i+1 >= prices.length) return total_sum;
//开始下一段的最大利益化
cur_segment_min = prices[i + 1];
if (context_min >= prices[i + 1]) {
//更正min
context_min = prices[i + 1];
//更正min_sum
context_min_sum = 0;
}
}
return total_sum;
}
}
总结
1)由简单到复杂,由易到难,这是一种好的训练方法。
2)感受复杂问题都是由小问题组成的,感受如何分析分解复杂问题。这些小问题不是独立的,是以一个核心问题不断加限制条件得来。
3)分析完问题,需要归纳整理好问题,防止编码时逻辑错乱导致出错。