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

题目

给你一个整数数组 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;
    }

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

相关推荐
Trouvaille ~1 天前
TCP Socket编程实战(三):线程池优化与TCP编程最佳实践
linux·运维·服务器·网络·c++·网络协议·tcp/ip
晓13131 天前
第六章 【C语言篇:结构体&位运算】 结构体、位运算全面解析
c语言·算法
iAkuya1 天前
(leetcode)力扣100 61分割回文串(回溯,动归)
算法·leetcode·职场和发展
June`1 天前
高并发网络框架:Reactor模式深度解析
linux·服务器·c++
梵刹古音1 天前
【C语言】 指针与数据结构操作
c语言·数据结构·算法
VT.馒头1 天前
【力扣】2695. 包装数组
前端·javascript·算法·leetcode·职场和发展·typescript
小镇敲码人1 天前
剖析CANN框架中Samples仓库:从示例到实战的AI开发指南
c++·人工智能·python·华为·acl·cann
刘琦沛在进步1 天前
【C / C++】引用和函数重载的介绍
c语言·开发语言·c++
我在人间贩卖青春1 天前
C++之this指针
c++·this
爱敲代码的TOM1 天前
数据结构总结
数据结构