算法——【最大子数组和】

题目

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

如图:

我们在刷题的时候经常会听到这么几个词:

子数组,子序列,连续子序列。他们的区别在哪里呢?

子数组:在原数组中连续的一段

子序列:不一定连续

连续子序列:就是子数组

这几个概念搞清楚,我们一起来看看这道题:

咱们以示例1为例:

示例1中的最大子数组就是[4,-1,2,1]这一段,他的元素和是6,是所有子数组中元素和最大的,所以6就是这个问题的答案。

这个答案是怎样求得的呢,接下来我介绍三种算法来解决这个问题。

方法一:暴力算法

核心思路:就是枚举所有可能的情况,因为子数组是连续的,我们可以固定起点,遍历所有的终点,计算每一段的和,再更新记录最大值。

比如:数组长度为 n ,我们用 i 表示子数组起点,从0到 n-1 依次遍历,每固定一个 i 就让 j 从 i 遍历到 n-1 ,同时累加从 i 到 j 的和,每算一次就和当前最大值比较并更新最大值,直到 i 遍历完所有位置,这样所有连续子数组都被我们检查了一遍,最后得到的最大值就是我们要求的答案了。

代码也非常的简单:

cpp 复制代码
int maxSubArray(vector<int>& nums) {
        int n =nums.size();
        int max = nums[0];
        for(int i=0;i<n;++i)
        {
            int add = 0;
            for(int j=i;j<n;++j)
            {
                add+=nums[j];
                if(add>max)
                {
                    max=add;
                }
            }
        }
        return max;
    }

暴力算法的优点就是逻辑简单易于理解,缺点就是时间复杂度太大(O(n²))。

当数组长度n很大时,作为题目提交的时候可能会超时,在面试中只写这个算法也是不够的,这时我们就要引入第二种算法了。

方法二:分治算法

核心思想:把大问题拆成小问题,然后小问题解决后再合并,最终得到大问题的答案。

对于这个题目,我们可以把数组从中间分成左右两个部分,那么最大子数组只会出现在三个地方:完全在左半部分、完全在右半部分、跨越中间位置(左半部分有一点右半部分有一点,拼接起来)。所以我们只需要分别求出这三个位置的最大子数组和,然后取其中的最大值,就是整个数组的最大子数组和。

对于左半部分,是一个大问题下的子问题,可以通过递归去求解;同样右半部分也是一样,所以两边都可以递归;重点是怎么求跨越中间的最大子数组和,

这个步骤需要单独处理:我们从中间位置向左遍历并累加元素,记录过程中的最大和;再从中间位置向右遍历并累加元素,记录过程中的最大和,然后把左最大和与右最大和相加就是跨越中间的最大和;最后在把这三个部分取个最大值就是这个问题的答案了。

核心是分治法+递归的调用:

cpp 复制代码
class Solution {
public:
    int maxSubArray(vector<int>& nums) {
        return getmaxSubArray(nums,0,nums.size()-1);
    }
private:
    int getmaxSubArray(vector<int>& nums,int left,int right)
    {
        //递归终止条件:子数组只有一个元素
        if(left == right)
        {
            return nums[left];
        }
        int mid = left +(right - left)/2;
        //递归计算左右子数组的最大子数组和
        int left_max = getmaxSubArray(nums,left,mid);
        int right_max = getmaxSubArray(nums,mid+1,right);
        //计算跨中点的最大子数组和
        int cross_left = INT_MIN;//INT_MIN是C++ 标准库<climits>中定义的常量,表示当前系统中int类型的最小取值
        int sum=0;
        for(int i=mid;i>=left;--i)
        {
            sum += nums[i];
            if(sum > cross_left)
            {
                cross_left = sum;
            }
        }

        int cross_right = INT_MIN;
        sum = 0;
        for(int i = mid+1;i<=right;++i)
        {
            sum+=nums[i];
            if(sum>cross_right)
            {
                cross_right = sum;
            }
        }
        int cross_max = cross_left + cross_right;
        //返回三者中的最大值
        return max(max(left_max,right_max),cross_max);
    }
};

这个算法的优点是时间复杂度是O(nlogn)的。相比暴力算法优化了很多;

缺点就是递归的逻辑较复杂,而且递归会有额外的栈开销。

方法三:Kadane算法(重点)

这是一个动态规划的解法,也是面试过程中最常考的最优解法。

思路:我们定义 dp[ i ] 为以第 i 个元素结尾的最大子数组和,那我们要求的答案就是 dp 数组中的元素最大值,对于 dp[ i ] 来说有两种选择:

第一种:把第 i 个元素加入到前面的子数组,也就是 dp[ i -1 ] + nums[ i ];

第二种:以第 i 个元素单独作为子数组,也就是 nums[ i ]。

所以dp[ i ]其实就是dp[ i -1 ] + nums[ i ] 和 nums[ i ]之间的一个最大值,也就是max(dp[ i -1 ] + nums[ i ] ,nums[ i ])。

那为什么是这两种选择?

因为子数组它一定是连续的,既然是连续的,以 i 结尾的子数组要么包含 i-1 结尾的子数组,要么自己单独成立一个。这个算法的精妙之处就在于只需要遍历数组一次就能算出所有的dp值,时间复杂度是O(n)的。

同样是考虑了所有情况,但是通过一种递推的思维把重复的计算都省略掉了。

代码:

cpp 复制代码
int maxSubArray(vector<int>& nums) {
        int n = nums.size();
        vector<int> dp(n);
        dp[0] = nums[0];
        int maxnum = dp[0];
        for(int i=1;i<n;++i)
        {
            //状态转移:要么延续前面的子数组,要么从当前元素重新开始
            dp[i] = max(dp[i-1]+nums[i],nums[i]);
            //更新全局最大和
            maxnum = max(maxnum,dp[i]);
        }
        return maxnum
    }
优化

因为要存dp数组,空间复杂度是O(n)的,但我们可以优化空间,因为计算dp[ i ]的时候,只需要用到dp[ i-1 ],所以不用保存整个数组,只需要用cur_max变量来记录 dp[ i-1 ],再用另外一个变量all_max来记录全局最大值,这样以来空间复杂度就降到了O(1)了。

代码:

cpp 复制代码
int maxSubArray(vector<int>& nums) {
        int n = nums.size();
        //初始化全局最大值为最小整数
        int all_max = INT_MIN;
        //初始化当前子数组和
        int cur_max = 0;

        for(int i = 1;i<=n;++i)
        {
            cur_max = max(cur_max + nums[i-1],nums[i-1]);
            all_max = max(all_max,cur_max);
        }
        return all_max;
    }

这些就是本篇文章的内容了,希望对大家的学习有帮助~~

相关推荐
rit84324991 小时前
UVE算法提取光谱特征波长的MATLAB实现与应用
开发语言·算法·matlab
tkevinjd1 小时前
力扣hot100-283移动零(盲人拉屎)
算法·leetcode
POLITE31 小时前
Leetcode 94. 二叉树的中序遍历 104. 二叉树的最大深度 226. 翻转二叉树 101. 对称二叉树 (Day 13)
算法·leetcode·职场和发展
老鼠只爱大米1 小时前
LeetCode经典算法面试题 #2:两数相加(迭代法、字符串修改法等多种实现方案详解)
算法·leetcode·链表·两数相加·字符串修改法·两数相减·大数运算
季明洵2 小时前
二分搜索、移除元素、有序数组的平方、长度最小的子数组
java·数据结构·算法·leetcode
XH华2 小时前
备战蓝桥杯,第一章:C++入门
c++·蓝桥杯
Sheep Shaun2 小时前
深入理解AVL树:从概念到完整C++实现详解
服务器·开发语言·数据结构·c++·后端·算法
_leoatliang2 小时前
基于Python的深度学习以及常用环境测试案例
linux·开发语言·人工智能·python·深度学习·算法·ubuntu
leiming62 小时前
C语言联合体union的用法(非常详细,附带示例)
java·python·算法