4、最大子数组和
1.问题描述
给你一个整数数组 nums ,请你找出一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。
子数组 是数组中的一个连续部分。
2.示例
示例 1:
输入:nums = [-2,1,-3,4,-1,2,1,-5,4]
输出:6
解释:连续子数组 [4,-1,2,1] 的和最大,为 6 。
示例 2:
输入:nums = [1]
输出:1
示例 3:
输入:nums = [5,4,-1,7,8]
输出:23
3.提示:
1 <= nums.length <= 105
-104 <= nums[i] <= 104
4.进阶:
如果你已经实现复杂度为 O(n) 的解法,尝试使用更为精妙的 分治法 求解。
5.具体解法(暴力循环,贪心算法,动态规划,分治法)
/*
方法一:暴力循环(不能AC,因为最后一个数组特大,运行时间超出要求)
//自己独立完成的代码,使用了暴力循环的方式,调试了三次终于从只能通过三分之一用例到三分之二到几乎全完成,到最后一次是超长数组通过不了(超出时间限制)
//对于调试代码有了新的理解和经验,一种是自身逻辑入手,一种是跟随失败案例循环一遍自己的代码看哪里哪一步出现了问题
//对于最后的超出时间,已经是超出我的目前水平了,那个数组得有好几千上万个数据,我这种暴力循环的思路,应该是不能解决这种问题的,而且自己写的是暴力循环中的比较笨的
//逻辑不够清晰,只能缝缝补补似的完成需求,不过这是个好的开始
方法一:自己想出来的最简单的暴力遍历,我们以四个数字为例,所有的连续子数组情况无非是(1,12,123,1234,2,23,234,3,34,4),
所以我们去按照这种思路遍历数组即可,定义一个max存储当前的最大值,
用sum去存储每一次计算的新的和,跟max比较,s>max就把s赋值给max,直到遍历结束,返回max
class Solution {
public int maxSubArray(int[] nums) {
int max=-999;//因为我后面是比较每个值跟他的大小,他来存储最大值,有一种数组里面全是负数,那么这个max的初始值就不适合设为0;
int s=0;
for(int i=0;i<nums.length;i++){//从i等于0开始循环,也就是队列的起始开始位置
if(i==nums.length-1){//如果是最后一个数,因为我会计算它加他后面的数,如果没有,应该会报错吧,所以我单独把这个拿出来进行判断了
s=nums[nums.length-1];
if(s>max){
max=s;
}
}
else{
int k=nums[i];//这个k的作用是存储刚刚加过的数,防止出现,每次都是挨着的两个数相加,而不是全部数相加的情况
for(int j=i;j<nums.length-1;j++){
if(nums[j]>max){
max=nums[j];//这里需要单独判断一下是不是第一个数就大于max,要不然如果是[2,-1]的这种情况,那么就会忽略掉max=2
}
s=k+nums[j+1];
k=s;
if(s>max){
max=s;
}
}
}
}
return max;//当所有的都遍历完成,那么就返回此时的最大值max
}
}
*/
/*
//补上一个可以成功提交的暴力解法
//可以对比一下,自己写的有多差
class Solution {
public int maxSubArray(int[] nums) {
int max = nums[0];
int n = nums.length;
for (int i = 0; i < n; i++) {
int sum = 0;
for (int j = i; j < n; j++) {
sum += nums[j];
if (sum > max){
max = sum;
}
}
}
return max;
}
}
*/
/*
//方法二:动态规划
//首先得去理解这种思路,找每个数作为结尾的数字的情况下的最大值即可
//用f(i)来表示以i为结尾的最大值
//为什么呢,以[1,2,3,4]为例,以1为结尾的就是[1],以2为结尾的可以是[1,2]或者[2],而[1,2]可以看做是问题1加上数字2
//再来一个例子,就更清晰了,以3为结尾的是[1,2,3]或[2,3]或[3],可以看做是问题2的两种情况加上数字3
//我们用一个变量pre来维持f(i) 动态规划状态转移式: f(i) = max{f(i-1) + num,num},我们还需要一个变量来存储接结果返回值
class Solution {
public int maxSubArray(int[] nums) {
int pre = 0, maxAns = nums[0];
for (int x : nums) {
pre = Math.max(pre + x, x);//这个是用来算每一个数做结尾的时候的最大值
maxAns = Math.max(maxAns, pre);//这个是比较i做结尾的最大值和当前的最大值,取大的作为新的最大值
}
return maxAns;
}
}
//动态规划的是首先对数组进行遍历,当前最大连续子序列和为 sum,结果为 ans
//如果 sum > 0,则说明 sum 对结果有增益效果,则 sum 保留并加上当前遍历数字
//如果 sum <= 0,则说明 sum 对结果无增益效果,需要舍弃,则 sum 直接更新为当前遍历数字
//每次比较 sum 和 ans的大小,将最大值置为ans,遍历结束返回结果
//这个理解也很好,就是我前面的sum如果小于零,那再去加新的数字肯定是不好的,所以直接把它舍掉,从最新的数字这里是最大值继续
//也有人说这种叫贪心,上面的才算dp(动态规划)
//下面这个说法非常非常好!!!!!!!!太容易去理解了
//其实这道题可以这么想: 1.假如全是负数,那就是找最大值即可,因为负数肯定越加越大。
//2.如果有正数,则肯定从正数开始计算和,不然前面有负值,和肯定变小了,所以从正数开始。
//3.当和小于零时,这个区间就告一段落了,然后从下一个正数重新开始计算(也就是又回到 2 了)。而 dp 也就体现在这个地方。
//贪心或者说也是一种动态规划的一个代码
class Solution {
public int maxSubArray(int[] nums) {
int ans = nums[0];
int sum = 0;
for(int num: nums) {
if(sum > 0) {
sum += num;
} else {
sum = num;
}
ans = Math.max(ans, sum);
}
return ans;
}
}
*/
//方法三:分治方法
/*
将原数组划分为左右两个数组后,原数组中拥有最大和的连续子数组的位置有三张情况。
情况1. 原数组中拥有最大和的连续子数组的元素都在左边的子数组中。
情况2. 原数组中拥有最大和的连续子数组的元素都在右边的子数组中。
情况3. 原数组中拥有最大和的连续子数组的元素跨越了左右数组。
分别求出,3中情况的最大和,取最大,就是原数组的连续子数组的最大和。
class Solution {
public int getMax(int[] nums, int low, int high) {
// 如果子数组只有一个元素,这个元素就是子树组的最大和。
if (low == high) {
return nums[low];
}
int mid = low + (high - low) / 2;
// 求左数组的最大和
int leftMax = getMax(nums, low, mid);
// 求右数组的最大和
int rightMax = getMax(nums, mid + 1, high);
// 求跨越情况的最大和
int crossMax = getCrossMax(nums, low, mid, high);
// 返回最大
return Math.max(Math.max(leftMax, rightMax), crossMax);
}
// 求跨越情况的最大和
public int getCrossMax(int[] nums, int low, int mid, int high) {
// 从中间向左走,一直累加,每次累计后都取最大值,最后得到的就是从中间向左累加可得到最大和
int leftSum = nums[mid];
int leftMax = nums[mid];
for (int i = mid - 1; i >= low; i--) {
leftSum += nums[i];
leftMax = Math.max(leftMax, leftSum);
}
// 从中间向右走,一直累加,每次累计后都取最大值,最后得到的就是从中间向右累加可得到最大和
int rightSum = nums[mid+1];
int rightMax = nums[mid+1];
for (int i = mid + 2; i <= high; i++) {
rightSum += nums[i];
rightMax = Math.max(rightMax, rightSum);
}
// 向左累加的最大和加上向右累加的最大和,就是跨越情况下的最大和
return leftMax + rightMax;
}
public int maxSubArray(int[] nums) {
return getMax(nums, 0 , nums.length - 1);
}
}
*/
/*
//分治思想的另一个代码
//官方给的代码,不如上面的好理解,没有注释
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) {
return getInfo(nums, 0, nums.length - 1).mSum;
}
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);
return pushUp(lSub, rSub);
}
public Status pushUp(Status l, Status r) {
int iSum = l.iSum + r.iSum;
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);
}
}
*/
6.收获
-
学习到了动态规划的概念,有了一个接触,但还不够掌握。更详细的解析可以看力扣解答第二个回答的讲解
-
复习了增强for的概念和使用
-
开发了自己的思路,第一次接近完全实现一次代码,熟练了对于代码的调试
-
题目只要求返回结果,不要求得到最大的连续子数组是哪一个。这样的问题通常可以使用「动态规划」解决。
-
动态规划的思路就是自底向上,从如果只有一个元素的最优解到有n个元素的最优解。
-
还接触到了贪心算法,对于贪心的理解也有了提升,是一种及时行乐的思想,
-
将分治算法与递归进行了一个区分理解:一个是不断调用自身,一个是将大问题分成小问题,然后分而治之,在分治中我看到也用到了递归调用的存在,不过递归只是整个分治中的一部分。
-
还复习了Math类的max方法,自己使用的时候就没有想到,而是自己用if语句去判断来着