121、难度简单:
题意:
某天买入股票,且在该天后的某天卖出。
实例1中的 " 你不能在买入前卖出股票 " 意为:你只有在买入一个股票后才能卖出股票
最多只允许完成一笔交易
方法一:暴力解法:超出时间限制
public class Solution {
public int maxProfit(int[] prices) {
int len = prices.length;
if (len < 2) {
return 0;
}
// 有可能不发生交易,因此结果集的初始值设置为 0
int res = 0;
// 枚举所有发生一次交易的股价差
for (int i = 0; i < len - 1; i++) {
for (int j = i + 1; j < len; j++) {
// 选取最高的利润作为返回值
res = Math.max(res, prices[j] - prices[i]);
}
}
return res;
}
}
方法二:动态规划:原理:
当天是否持股 是一个很重要的因素,而当前是否持股和昨天是否持股有关系,为此我们需要把 是否持股 设计到状态数组中。
dp[ i ] [ j ]:下标为 i
这一天结束的时候,手上持股状态为 j
时,我们持有的现金数(利润)。
j = 0,表示当前不持股;
j = 1,表示当前持股。
这个状态具有前缀性质,下标为 i
的这一天的计算结果包含了区间 [0, i]
所有的信息,因此最后输出 dp[len - 1][0]
。
(也就是每项 dp[ i ] [ j ] 的值代表了到目前为止的最大利润)
dp[ i ] [ 0 ]:规定了今天不持股,有以下两种情况:
昨天不持股,今天什么都不做;
昨天持股,今天卖出股票(现金数增加)
dp[ i ] [ 1 ]:规定了今天持股,有以下两种情况:
昨天持股,今天什么都不做(现金数与昨天一样);
昨天不持股,今天买入股票(注意:只允许交易一次,因此手上的现金数就是当天的股价的相反数)
public class Solution {
public int maxProfit(int[] prices) {
int len = prices.length;
// 数组元素只有0个或1个时返回0
if (len < 2) {
return 0;
}
int[][] dp = new int[len][2];
// dp[i][0] 下标为 i 这天结束的时候,不持股,手上拥有的现金数
// dp[i][1] 下标为 i 这天结束的时候,持股,手上拥有的现金数
// 针对第一天的情况:不持股显然为 0,持股就需要减去第 1 天(下标为 0)的股价
dp[0][0] = 0;
dp[0][1] = -prices[0];
// 从第 2 天开始遍历
for (int i = 1; i < len; i++) {
// 由 max 方法实现原理中的 "每项 dp[i] [j] 的值代表了到目前为止的最大利润
// 比较前一天不持股拥有的利润 和 前一天持股今天卖出拥有的利润,选取大的
dp[i][0] = Math.max(dp[i - 1][0], dp[i - 1][1] + prices[i]);
// 比较前一天持股拥有的利润 和 前一天不持股今天买入股票拥有的利润,选取大的
dp[i][1] = Math.max(dp[i - 1][1], -prices[i]);
}
//最后一天只能是不持股的状态(前面买入今天卖出),而每项 dp[i] [j] 的值代表了到目前为止的最大利润,所以直接返回即可
return dp[len - 1][0];
}
}
后续优化请点击下面链接:
作者:liweiwei1419
链接:https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock/solution/bao-li-mei-ju-dong-tai-gui-hua-chai-fen-si-xiang-b/
方法三:一次遍历:方法二在执行用时、内存消耗中击败用户比例较少,方法三则较多
原理:
假设给定的数组为:[7, 1, 5, 3, 6, 4]
如果我们在图表上绘制给定数组中的数字,我们将会得到:
假如计划在第 i 天卖出股票,那么最大利润的差值一定是在[0, i-1] 之间选最低点买入;
所以遍历数组,依次求每个卖出时机的的最大差值,再从中取最大值。
public class Solution {
public int maxProfit(int prices[]) {
// 今天卖出股票前的股票价格最低点
int minprice = Integer.MAX_VALUE;
// 最大利润
int maxprofit = 0;
for (int i = 0; i < prices.length; i++) {
// 不断刷新买入最低点
if (prices[i] < minprice) {
minprice = prices[i];
//若今天卖出得到的利润大于历史最高利润,那就更替最大利润值
} else if (prices[i] - minprice > maxprofit) {
maxprofit = prices[i] - minprice;
}
}
return maxprofit;
}
}
53、难度简单:
要求:若已经实现复杂度为 O(n)
的解法,尝试使用更为精妙的 分治法 求解
方法一:动态规划 / 贪心
原理:
1.假如全是负数,那就是找最大值即可,因为负数肯定越加越小。
2.如果有正数,则肯定从正数开始计算和,不然前面有负值,和肯定变小了,所以从正数开始。
3.当和小于零时,这个区间就告一段落了,然后从下一个正数重新开始计算(也就是又回到步骤 2 了)
class Solution {
public int maxSubArray(int[] nums) {
// ans是用来记录sum的最大值
int ans = nums[0];
// sum是和的意思
int sum = 0;
for(int num: nums) {
if(sum > 0) {
// 当sum > 0 时, 就可以对后面的数产生增益,所以给他加上
sum += num;
} else {
// 当sum < 0 时, 就已经没有再往后加的必要了,这个时候直接把下一个值拿过来赋值给sum
sum = num;
}
// 比较一下sum和ans的大小,取最大值
ans = Math.max(ans, sum);
}
return ans;
}
}
代码过程:
开始时,sun=0、ans=nums[0],若遍历nums数组时无论第一个元素是正是负,sum都不大于0所以赋值为num
若nums的第一个元素为负,那么sum此时也为负,遍历到第二个元素时sum也不大于0所以继续赋值num给sum
直到num为正为止,下一轮的sum才会大于0(体现了从正数开始计算和)
ans可以作为在当前轮次之前的最大值,而sum则是进行时的当前轮次的总值。
若sum一开始遇到整数,但之后遇到负数,只要这个负数不足以让sum直接为负那就继续执行sum的数值添加(因为添加了负数所以sum变小了,所以不会改变ans的值);若使其为负,那么当前轮次就可以结束了(反正ans已经记录了sum在变负之前的最大值),直接以下一个元素作为sum新的开始(新的轮次)
方法二:分治:
原理:分治法就是把一个大问题转化为很多小问题:
首先举三个例子:我们先把数组一分为二,求出左半部分和右半部分各自的最大子序和。
-1、 2、 3、|-9、-6 //左半部分为 5,右半部分为-6,总的最大子序和为左半部分的 5
-9、-6、-1、| 2、 3 //左半部分为-1,右半部分为 5,总的最大子序和为右半部分的 5
-1、-9、 2、| 3、 4、-6 //左半部分为 2,右半部分为 7,总的最大子序和为两侧交接部分的 9
由此我们可以根据将数组划分为:左边、右边、中间三个方位来分别计算最大子序和最终求出整个数组的最大子序和
此时我们再假设一个例子:
-2 1 4 1 2 -3 //原数组
-2 1 4 | 1 2 -3 //中间一分为二
-2 1 | 4 | 1 2 |-3 //左、右边一分为二
-2 | 1 | 4 | 1| 2 |-3 //一直分到每个区域只有一个元素为止
对上述划分出的区域,从底下往上开始推算:
第四行:第一区域的最大子序和为-2;第二区域为1;第三为4;…
第三行:第一区域的左区域最大子序和为-2,右右区域为1,中间合并为-1,可得最大子序和为1;第二区域为4;第三区域为中间合并的1+2=3;…
第二行:第一区域的左区域最大子序和为1,右区域为4,中间线往左数最大子序和为1,往右数最大子序和为4,所以合并为1+4=5,可得最大子序和为5;
第二区域的左区域最大子序和为3,右区域为-3,中间线往左数最大子序和为3,往右数最大子序和为-3,所以合并为0,可得最大子序和为3;
第三区域的的左区域最大子序和为5,右区域为3,中间线往左数最大子序和为5,往右数最大子序和为3,所以合并为8,可得最大子序和为8;
原理总结:
假定我们现在期望求解区间M:[l, r]上的最大连续子列和mSum,按照分治算法的思路,应当通过划分[l, r]为左区间L:[l, mid]和右区间R:[mid + 1, r],递归地求解出L.mSum以及R.mSum之后求解M.mSum。
存在三种可能:
M上的最大连续子列和序列完全在L中,即M.mSum = L.mSum
M上的最大连续子列和序列完全在R中,即M.mSum = R.mSum
M上的最大连续子列和序列横跨L和R,则该序列一定是从L中的某一位置开始延续到mid(L的右边界),然后从mid + 1(R的左边界)开始延续到R中的某一位置。因此我们还需要维护区间左边界开始的最大连续子列和 leftSum 以及区间右边界结束的最大连续子列和 rightSum 信息
综上可以得到M.mSum = max(L.mSum, R.mSum, L.rightSum + R.leftSum)
M.leftSum对应的序列必须从M的左边界开始,也就是必须从L的左边界开始,如果该序列在L中就结束了,那么M.leftSum = L.leftSum,而如果延续到了R中,其值就是L中所有数值之和再加上R.leftSum了,因此我们需要维护区间所有数值之和totalSum的信息,这样就得到了M.leftSum = max(L.leftSum, L.totalSum + R.leftSum),对偶地,我们可以得到M.rightSum = max(R.rightSum, R.totalSum + L.rightSum)
其余同理
class Solution {
// 作为方法的返回值类型
public class Status {
// 属性:左边最大子段、右边最大子段、总区域的最大子段、总区间所有数的和
public int lSum, rSum, mSum, iSum;
// 构造器
public Status(int lSum, int rSum, int mSum, int iSum) {
this.lSum = lSum;
this.rSum = rSum;
this.mSum = mSum;
this.iSum = iSum;
}
}
// 开始的方法
public int maxSubArray(int[] nums) {
// 最终我们要求的答案就是 nums 序列 [0, nums.length() - 1] 区间的最大子段和
// 因为 getInfo 方法返回 Status 类型,而我们只需要其中的 mSum 属性值
return getInfo(nums, 0, nums.length - 1).mSum;
}
// 分治法的分治函数,返回 Status 类型
// 用于查询 a 序列 [l,r] 区间内的最大子段和
public Status getInfo(int[] a, int l, int r) {
// 当每个区间里只有一个元素时,该元素同时等于四种最大子段,返回该元素
if (l == r) {
return new Status(a[l], a[l], a[l], a[l]);
}
// 取中间
int m = (l + r) >> 1;
// 将左边作为新的区域,并调用自己,开始递归
Status lSub = getInfo(a, l, m);
// 将右边作为新的区域,并调用自己,开始递归
Status rSub = getInfo(a, m + 1, r);
// 调用求最大子段的方法,并传入要求的区域
// 也就是把传入的 a 序列的 [l,r] 区间划分为左右两个区间
// 由于递归,所以又把左右区间各自又取了左右区间,直至每个区间只有一个元素
// 然后开始逐级返回
// 最终返回的是综合 a 序列 [l,r] 区间内左边最大子段、右边最大子段、总区域的最大子段、总区间所有数的和的Status
return pushUp(lSub, rSub);
}
// 求区域最大子段的方法,返回 Status 类型
// 传入要求区域被划分出的左右区域
public Status pushUp(Status l, Status r) {
// 将左右区间各自的总值加起来,就是所求区间的总值
int iSum = l.iSum + r.iSum;
// 对于 [l,r] 的 lSum,存在两种可能:
// 它要么等于「左子区间」的 lSum,
// 要么等于「左子区间」的 iSum 加上「右子区间」的 lSum,二者取大
int lSum = Math.max(l.lSum, l.iSum + r.lSum);
int rSum = Math.max(r.rSum, r.iSum + l.rSum);
int mSum = Math.max(Math.max(l.mSum, r.mSum), l.rSum + r.lSum);
return new Status(lSum, rSum, mSum, iSum);
}
}