53. 最大子序和(剑指 Offer 42)

知识点:数组;前缀和;哨兵;动态规划;贪心;分治

题目描述

输入一个整型数组,数组中的一个或连续多个整数组成一个子数组。求所有子数组的和的最大值。

要求时间复杂度为O(n)。

示例
  1. 输入: nums = [-2,1,-3,4,-1,2,1,-5,4]
  2. 输出: 6
  3. 解释: 连续子数组 [4,-1,2,1] 的和最大,为 6

解法一:前缀和+哨兵

连续子数组 --> 前缀和

从前往后遍历求前缀和,维持两个变量,一个是最大子数组和,也就是答案,一个是最小的前缀和,我们可以把这个值理解为哨兵,这个就是我们用来获取答案的,因为每次前缀和-这个最小的肯定就是最大的。

  1. class Solution {
  2. public int maxSubArray(int[] nums) {
  3. int pre = 0; //前缀和;
  4. int minPre = 0; //最小的前缀和:哨兵;
  5. int maxSum = Integer.MIN_VALUE;
  6. for(int i = 0; i < nums.length; i++){
  7. pre += nums[i];
  8. maxSum = Math.max(maxSum, pre-minPre);
  9. minPre = Math.min(pre, minPre);
  10. }
  11. return maxSum;
  12. }
  13. }

解法二:贪心

这道题贪心怎么解?贪什么呢?想一下在这个过程中,比如-2 1,我们需要-2吗?不需要!因为负数只会拉低我们最后的和,只起副作用的索性不如不要了。直接从1开始就行了; 贪的就是负数和一定会拉低结果。

所以我们的贪心选择策略就是:只选择和>0的,对于和<=0的都可以舍弃了。

  1. class Solution {
  2. public int maxSubArray(int[] nums) {
  3. int maxSum = Integer.MIN_VALUE;
  4. int sum = 0;
  5. for(int i = 0; i < nums.length; i++){
  6. sum += nums[i];
  7. maxSum = Math.max(sum, maxSum);
  8. if(sum <= 0){
  9. sum = 0; //对于<=0的前缀和,已经没要意义了,从下一位置开始;
  10. continue;
  11. }
  12. }
  13. return maxSum;
  14. }
  15. }

解法三:分治

这道题可以用分治去解。期望去求解一个区间[l,r]内的最大子序和,按照分而治之的思想,可以将其分为左区间和右区间。

左区间L:[l, mid]和右区间R:[mid + 1, r].

lSum 表示 [l,r] 内以 l 为左端点的最大子段和

rSum 表示 [l,r] 内以 r 为右端点的最大子段和

mSum 表示 [l,r] 内的最大子段和

iSum 表示 [l,r] 的区间和

递归地求解出L.mSum以及R.mSum之后求解M.mSum。因此首先在分治的递归过程中需要维护区间最大连续子列和mSum这个信息。

接下来分析如何维护M.mSum。具体来说有3种可能:

  • M上的最大连续子列和序列完全在L中,即M.mSum = L.mSum
  • M上的最大连续子列和序列完全在R中,即M.mSum = R.mSum
  • M上的最大连续子列和序列横跨L和R,则该序列一定是从L中的某一位置开始延续到mid(L的右边界),然后从mid + 1(R的左边界)开始延续到R中的某一位置。因此我们还需要维护区间左边界开始的最大连续子列和leftSum以及区间右边界结束的最大连续子列和rightSum信息
  1. class Solution {
  2. public class Status{
  3. public int lSum, rSum, mSum, iSum;
  4. // lSum 表示 [l,r] 内以 l 为左端点的最大子段和
  5. // rSum 表示 [l,r] 内以 r 为右端点的最大子段和
  6. // mSum 表示 [l,r] 内的最大子段和
  7. // iSum 表示 [l,r] 的区间和
  8. public Status(int lSum, int rSum, int mSum, int iSum){
  9. this.lSum = lSum;
  10. this.rSum = rSum;
  11. this.mSum = mSum;
  12. this.iSum = iSum;
  13. }
  14. }
  15. public Status getInfo(int[] a, int l, int r){
  16. if(l == r) return new Status(a[l], a[l], a[l], a[l]); //终止条件;
  17. int mid = l + ((r-l) >> 1);
  18. Status lsub = getInfo(a, l, mid);
  19. Status rsub = getInfo(a, mid+1, r);
  20. return pushUp(lsub, rsub);
  21. }
  22. //根据两个子串得到整个序列结果;
  23. public Status pushUp(Status l, Status r){
  24. int iSum = l.iSum + r.iSum;
  25. int lSum = Math.max(l.lSum, l.iSum+r.lSum);
  26. int rSum = Math.max(r.rSum, r.iSum+l.rSum);
  27. int mSum = Math.max(Math.max(l.mSum, r.mSum), l.rSum+r.lSum);
  28. return new Status(lSum, rSum, mSum, iSum);
  29. }
  30. public int maxSubArray(int[] nums) {
  31. return getInfo(nums, 0, nums.length-1).mSum;
  32. }
  33. }

解法四:动态规划

  • 1.确定dp数组和其下标的含义:dp[i]表示以i结尾的连续子数组的最大和;
  • 2.确定递推公式,即状态转移方程:以i结尾想一下我们有几种可能,一种是i-1过来的,也就是上一个的连续子数组延续到i处了,那和就为dp[i-1]+nums[i],另一种呢,就是自己开始,前面那个连续子数组不行,那就是nums[i]了,想一下为什么前面那个不行,还不是前面的和会拖累自己,那就意味着前面的和是负数;这其实就引出贪心的方法了。不过我们这里不用这么麻烦,直接用一个max函数,取两者大的那个就行;
  • 3.dp初始化base case:dp[0]只有一个数,所以dp[0] = nums[0];
  1. class Solution {
  2. public int maxSubArray(int[] nums) {
  3. int len = nums.length;
  4. int[] dp = new int[len]; //以i结尾的连续子数组的最大和为dp[i];
  5. if(nums == null || len <= 1) return nums[0];
  6. dp[0] = nums[0];
  7. for(int i = 1; i < len; i++){
  8. dp[i] = Math.max(dp[i-1]+nums[i], nums[i]); //状态转移;
  9. }
  10. //注意我们要遍历一遍返回最大的dp;
  11. int maxSum = dp[0];
  12. for(int i = 1; i < len; i++){
  13. maxSum = Math.max(maxSum, dp[i]);
  14. }
  15. return maxSum;
  16. }
  17. }

当然上述程序可以优化,因为我们的dp[i]其实只和前一状态i-1有关,所以可以采用一个滚动变量来记录,而不用整个数组。

  1. class Solution {
  2. public int maxSubArray(int[] nums) {
  3. int pre = 0; //记录前一状态;
  4. int res = nums[0]; //记录最后结果的最大值;
  5. for (int num : nums) {
  6. pre = Math.max(pre + num, num);
  7. res = Math.max(res, pre);
  8. }
  9. return res;
  10. }
  11. }

体会

这道题目是一道很典型的题目,用到了各种方法和思想。要常看常做,分治是其中比较困难的,但是要会这种思想。这道题目最好的方法还是哨兵和动态规划, 其实贪心就是从动态规划的一个特殊情况过去的,体会两者的关系;

53. 最大子序和(剑指 Offer 42)的更多相关文章

  1. 刷题-力扣-剑指 Offer 42. 连续子数组的最大和

    剑指 Offer 42. 连续子数组的最大和 题目链接 来源:力扣(LeetCode) 链接:https://leetcode-cn.com/problems/lian-xu-zi-shu-zu-de ...

  2. 剑指 Offer 42. 连续子数组的最大和 + 动态规划

    剑指 Offer 42. 连续子数组的最大和 题目链接 状态定义: 设动态规划列表 \(dp\) ,\(dp[i]\) 代表以元素 \(4nums[i]\) 为结尾的连续子数组最大和. 为何定义最大和 ...

  3. 力扣 - 剑指 Offer 42. 连续子数组的最大和

    题目 剑指 Offer 42. 连续子数组的最大和 思路1(分析数组的规律) 我们可以从头到尾逐个累加,若之前的累加和小于0,那就从丢弃之前的累加,从当前开始重新累加,同时在遍历过程中比较记录下最大值 ...

  4. 【Java】 剑指offer(42) 连续子数组的最大和

    本文参考自<剑指offer>一书,代码采用Java语言. 更多:<剑指Offer>Java实现合集   题目 输入一个整型数组,数组里有正数也有负数.数组中一个或连续的多个整/ ...

  5. 剑指Offer 42. 和为S的两个数字 (其他)

    题目描述 输入一个递增排序的数组和一个数字S,在数组中查找两个数,使得他们的和正好是S,如果有多对数字的和等于S,输出两个数的乘积最小的. 输出描述: 对应每个测试案例,输出两个数,小的先输出. 题目 ...

  6. [剑指Offer] 42.和为S的两个数字

    题目描述 输入一个递增排序的数组和一个数字S,在数组中查找两个数,是的他们的和正好是S,如果有多对数字的和等于S,输出两个数的乘积最小的. 输出描述: 对应每个测试案例,输出两个数,小的先输出. [思 ...

  7. 剑指offer——42最小的K个数

    题目描述 输入n个整数,找出其中最小的K个数.例如输入4,5,1,6,2,7,3,8这8个数字,则最小的4个数字是1,2,3,4,.   题解: 原以为书中会有好方法,想不到还是排序和STL这两种方法 ...

  8. 每日一题 - 剑指 Offer 42. 连续子数组的最大和

    题目信息 时间: 2019-06-30 题目链接:Leetcode tag: 动态规划 难易程度:简单 题目描述: 输入一个整型数组,数组里有正数也有负数.数组中的一个或连续多个整数组成一个子数组.求 ...

  9. 剑指 Offer 42. 连续子数组的最大和

    题目描述 输入一个整型数组,数组中的一个或连续多个整数组成一个子数组.求所有子数组的和的最大值. 要求时间复杂度为\(O(n)\). 示例1: 输入: nums = [-2,1,-3,4,-1,2,1 ...

随机推荐

  1. 乘风破浪,遇见Visual Studio 2022预览版(Preview),宇宙最强开发者工具首次迎来64位版本

    简介 众所周知,我们从官方新闻来看,对Visual Studio 2022最大的期待莫过于:其是首个64位的Visual Studio,这个宇宙最强开发者工具一脚迈入了新的阶段. https://vi ...

  2. TS基础应用 & Hook中的TS

    说在前面 本文难度偏中下,涉及到的点大多为如何在项目中合理应用ts,小部分会涉及一些原理,受众面较广,有无TS基础均可放心食用. **>>>> 阅完本文,您可能会收获到< ...

  3. 6、负载均衡HAproxy介绍

    6.1.常用负载均衡器软件说明: 现在常用的三大开源软件负载均衡器分别是Nginx.HAProxy.LVS. 1.nginx负载均衡的特点: (1)工作在网络的七层之上,可以针对http应用做一些分流 ...

  4. 32、JavaScript介绍

    32.1.JavaScript概述: 1.JavaScript的历史: 1992年Nombas开发出C-minus-minus(C--)的嵌入式脚本语言(最初绑定在CEnvi软件中),后将其改名Scr ...

  5. Redis 过期时间解析

    文章参考:<Redis 设计与实现>黄建宏 设置过期时间 通过 EXPIRE 或者 PEXPIRE 命令,客户端可以以秒或毫秒精度为数据库中的某个键设置生存时间 TTL (Time To ...

  6. 无法push项目到gitlab的解决方案

    gitlab项目组下创建项目 $ git push -u git@192.168.101.129:/DrvOps/Dev_Test : 报错信息如下: remote: ================ ...

  7. Web 前端开发规范手册

    一.规范目的 Web 前端开发规范手册 1.1 概述 ......................................................................... ...

  8. 我用段子讲.NET之依赖注入其二

    <我用段子讲.NET之依赖注入其二> "随着我们将业务代码抽象化成接口和实现两部分,这也使得对象生命周期的统一管理成为可能.这就引发了第二个问题,.NET Core中的依赖注入框 ...

  9. 令牌桶限流思路分享(PHP+Redis实现机制)

    一 .场景描述 在开发接口服务器的过程中,为了防止客户端对于接口的滥用,保护服务器的资源, 通常来说我们会对于服务器上的各种接口进行调用次数的限制.比如对于某个 用户,他在一个时间段(interval ...

  10. Java | 参数传值机制

    值传递 java中,方法中所有的参数的都是"值传递",就是传递的是原来值的副本,不是原来的参数,因此,改变不会影响到原来的参数. 基本数据类型参数的传值 传递的都是副本,改变以后不 ...