Leetcode-数据结构-53.最大子数组和

问题

//给一个整数数组 nums , // 请找出一个具有"最大和"的连续子数组(子数组最少包含一个元素), // 返回其最大和。

//子数组 是数组中的一个"连续部分"

分析

// 这是一道典型的使用「动态规划」

(连续、只要结果)解决的问题,

// 需要我们掌握动态规划问题设计状态的技巧(无后效性),

// 并且需要知道如何推导状态转移方程,

// 最后再去优化空间。

转换为若干个 子问题

连续:可以求出 ”所有“ 经过(以**结尾的)输入数组的 某一个数 的连续子数组的最大和(只要这个结果)。

如果编号为 i 的子问题的结果是负数或者 0 ,

那么编号为 i + 1 的子问题就可以把编号为 i 的子问题的结果”舍弃掉“(因为要求是最大和)

代码

main函数

package DataStructure_start;

public class DS20230109 {

    public static void main(String[] args) {
        int[] nums = {-1,9,-2,3,5,6};
        System.out.println(maxSubArray(nums));
        System.out.println(maxSubArray1(nums));
        System.out.println(maxSubArray2(nums));
        System.out.println(maxSubArray3(nums));
    }
}

方法一:动态规划

参考1:(子数组)

时间复杂度:O(n),N是输入数组的长度

public static int maxSubArray(int[] nums) {
        int len = nums.length;
        // dp[i] 表示:以 nums[i] 结尾的连续子数组的最大和
        int[] dp = new int[len];
        dp[0] = nums[0];

//        如果编号为 i 的子问题的结果是负数或者 0 ,
//        那么编号为 i + 1 的子问题就可以把编号为 i 的子问题的结果”舍弃掉“(因为要求是最大和)
//        即:如果i的子问题的结果是正数,则保留
        for (int i = 1; i < len; i++) {
//            如果前一个数为正数
            if (dp[i - 1] > 0) {
//                则再加下一个数
//                ?但只是相邻的两个数 ×
//                  dp[i] 表示:以 nums[i] 结尾的连续子数组的最大和
//                  注意区别dp[](一个连续的数组)与num[](一个数)的区别
                dp[i] = dp[i - 1] + nums[i];
            } else {
                dp[i] = nums[i];
            }
        }

        // 也可以在上面遍历的同时求出 res 的最大值,这里我们为了语义清晰分开写,大家可以自行选择
        int res = dp[0];
        for (int i = 1; i < len; i++) {
            res = Math.max(res, dp[i]);//调用函数Math:max函数:判断谁是最大值(三元表达式)
        }
        return res;//返回最大和
    }

补充:

三元表达式

public static int max(int a, int b) {

return (a >= b) ? a : b;

}

时间复杂度从小到大排序:

(由里向外分析时间复杂度;取最大的时间复杂度)

O(1) < O(logn) < O(n) < O(nlogn) < O(n^2) < O(n^3) < O(2^n) < O(n!) < O(n^n)

参考2:优化后

时间复杂度:O(n),N是输入数组的长度

public static int maxSubArray1(int[] nums) {
        int pre = 0;//前一个数
        int res = nums[0];//最大和

//            只适用于数组,循环累加(优化点★)
        for (int num : nums) {
            pre = Math.max(pre + num, num);
            res = Math.max(res, pre);
        }
        return res;
    }

有后效性:

如果之前的阶段求解的子问题的结果包含了一些不确定的信息,导致了后面的阶段求解的子问题无法得到,或者很难得到,这叫「有后效性」

解决「有后效性」的办法是固定住需要分类讨论的地方,记录下更多的结果。在代码层面上表现为:

状态数组增加维度;

把状态定义得更细致、准确:状态定义只解决路径来自左右子树的其中一个子树。

需要经常思考 为什么想到需要这样定义状态。

动态规划的是首先对数组进行遍历,当前最大连续子序列和为 sum,结果为 ans

如果 sum > 0,则说明 sum 对结果有增益效果,则 sum 保留并加上当前遍历数字

如果 sum <= 0,则说明 sum 对结果无增益效果,需要舍弃,则 sum 直接更新为当前遍历数字

每次比较 sum 和 ans的大小,将最大值置为ans,遍历结束返回结果

时间复杂度:O(n)

public static int maxSubArray3(int[] nums) {

        int ans = nums[0];
        int sum = 0;

//        此处类似方法一参考1中的优化部分,即for循环数组
        for(int num: nums) {

//          正数
            if(sum > 0) {
//              累加sum
                sum += num;
//              负数,保持不变
            } else {
                sum = num;
            }

//          三元表达式:比较取最大值
            ans = Math.max(ans, sum);
        }
        return ans;
    }

方法二:分治法(分类讨论,分成三部分)

复杂度分析:

时间复杂度:O(NlogN),这里递归的深度是对数级别的,每一层需要遍历一遍数组(或者数组的一半、四分之一);

空间复杂度:O(logN),需要常数个变量用于选取最大值,需要使用的空间取决于递归栈的深度。

连续子序列的最大和主要由这三部分子区间里元素的最大和得到:

第 1 部分:子区间 [left, mid];

第 2 部分:子区间 [mid + 1, right];

第 3 部分:包含子区间 [mid , mid + 1] 的子区间,

即 nums[mid] 与 nums[mid + 1] 一定会被选取(因为跨区。从中间向两边扩散,扩散到底 选出最大值)。

对这三个部分求最大值即可。

public static int maxSubArray2(int[] nums) {
        int len = nums.length;
        if (len == 0) {
            return 0;
        }
        return maxSubArraySum(nums, 0, len - 1);
    }

    private static int maxCrossingSum(int[] nums, int left, int mid, int right) {
        // 一定会包含 nums[mid] 这个元素
        int sum = 0;
        int leftSum = Integer.MIN_VALUE;
        // 左半边包含 nums[mid] 元素,最多可以到什么地方
        // 走到最边界,看看最值是什么
        // 计算以 mid 结尾的最大的子数组的和
        for (int i = mid; i >= left; i--) {
            sum += nums[i];
            if (sum > leftSum) {
                leftSum = sum;
            }
        }
        sum = 0;
        int rightSum = Integer.MIN_VALUE;
        // 右半边不包含 nums[mid] 元素,最多可以到什么地方
        // 计算以 mid+1 开始的最大的子数组的和
        for (int i = mid + 1; i <= right; i++) {
            sum += nums[i];
            if (sum > rightSum) {
                rightSum = sum;
            }
        }
        return leftSum + rightSum;
    }

    private static int maxSubArraySum(int[] nums, int left, int right) {
        if (left == right) {
            return nums[left];
        }
        int mid = left + (right - left) / 2;
        return max3(maxSubArraySum(nums, left, mid),
                maxSubArraySum(nums, mid + 1, right),
                maxCrossingSum(nums, left, mid, right));
    }

    private static int max3(int num1, int num2, int num3) {
        return Math.max(num1, Math.max(num2, num3));
    }

参考

作者:liweiwei1419

链接:https://leetcode.cn/problems/maximum-subarray/solution/dong-tai-gui-hua-fen-zhi-fa-python-dai-ma-java-dai/

来源:力扣(LeetCode)

作者:guanpengchn

链接:https://leetcode.cn/problems/maximum-subarray/solution/hua-jie-suan-fa-53-zui-da-zi-xu-he-by-guanpengchn/

来源:力扣(LeetCode)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值