【Leetcode】最大子序和问题(含最大和为负数情况)6种解法汇总(C++实现)


前言

    最大子列和问题是数据结构与算法分析中最经典的题目之一,其解法非常多,许多算法的精髓都能在这一问题中得到体现。本文将对该题的不同解法做一个汇总,同时增加了最大值为负数时的情况,方便自己日后复习。这篇文章如果对你有用的话记得一键三连哦!!


原题重现

  • 通俗表达:给定一个整数数组nums( 1 ≤ n u m s . l e n g t h ≤ 3 ∗ 1 0 4 1 \le nums.length \le 3 * 10^4 1nums.length3104 − 1 0 5 ≤ n u m s [ i ] ≤ 1 0 5 -10^5 \le nums[i] \le 10^5 105nums[i]105),找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。

  • 数学表达:给定N个整数的序列{ A 1 , A 2 , . . . , A n A_1, A_2, ..., A_n A1,A2,...,An},求函数 f ( i , j ) = ∑ k = i j A k ( i ≤ j ) f(i,j)=\sum_{k=i}^jA_k(i \le j) f(i,j)=k=ijAk(ij)的最大值。


解法1:暴力法

    该方法没有什么可说的,直接利用三重循环求出所有子列的和,取出最大值。C++代码如下:

#include <iostream>
#include <algorithm>
#include <vector>

using namespace std;

class Solution {
public:
    static int maxSubArray(vector<int> &nums) {
        int sum = 0, maxSum = -100001;
        for (int i = 0; i < nums.size(); i++) {
            for (int j = 0; j < nums.size(); j++) {
                for (int k = i; k <= j; k++) {
                    sum += nums[k];
                    maxSum = max(maxSum, sum);
                }
                sum = 0;
            }
        }
        return maxSum;
    }
};



int main() {
    int arr[] = {-2, 1, -3, 4, -1, 2, 1, -5, 4};
    vector<int> nums = vector<int>(arr, arr + 9);

    cout << Solution::maxSubArray(nums);
    return 0;
}

  • 时间复杂度: O ( n 3 ) O(n^3) O(n3)

解法2:优化版暴力法

    解法1中的第三个for循环全然没有必要,在第二层for循环的时候就计算求得的和并且继续带入下一轮的for循环就可以。代码如下:

#include <iostream>
#include <algorithm>
#include <vector>

using namespace std;

class Solution {
public:
    static int maxSubArray(vector<int> &nums) {
        int sum = 0, maxSum = -100001;
        for (int i = 0; i < nums.size(); i++) {
            sum = 0;
            for (int j = i; j < nums.size(); j++) {
                sum += nums[j];
                maxSum = max(maxSum, sum);
            }
        }
        return maxSum;
    }
};


int main() {
    int arr[] = {-2, 1, -3, 4, -1, 2, 1, -5, 4};
    vector<int> nums = vector<int>(arr, arr + 9);

    cout << Solution::maxSubArray(nums);
    return 0;
}
  • 时间复杂度: O ( n 2 ) O(n^2) O(n2)

解法3:分治法

分治法:核心思想是将一个大问题拆解为若干个子问题进行递归求解,最后再将所有子问题的解进行合并,从而得到原问题的结果。

    对于本问题而言,一个序列的最大子序列的和可能来自三个地方:左半边子列、右半边子列、跨越中间的子列。如下图所示:
分治法示意图
    *图源:中国大学MOOC 浙江大学《数据结构》

    想到这里,接下来使用递归就可以进行求解。代码如下:

#include <iostream>
#include <algorithm>
#include <vector>
#include <string>

using namespace std;

class Solution {
public:
    static int maxSubArray(vector<int> &nums, int left, int right) {
        // 只有一个元素,直接输出
        if (left == right) {
            return nums[left];
        }

        // 求出中心点位置
        int center = (left + right) / 2;

        // 递归求解左半边子列和
        int leftMaxSum = maxSubArray(nums, 0, center);

        // 递归求解右半边子列和
        int rightMaxSum = maxSubArray(nums, center + 1, right);

        // 求出跨越中间的最大子列和,因为是连续子列,故需要从中间点向两边延伸求和
        // 从中间出发向左求出最大和
        int maxLeftSum = 0;
        int leftSum = 0;
        for (int i = center; i >= left; i--) {
            leftSum += nums[i];
            maxLeftSum = max(maxLeftSum, leftSum);
        }

        // 从中间出发向右求出最大和
        int maxRightSum = 0;
        int RightSum = 0;
        for (int i = center + 1; i <= right; i++) {
            RightSum += nums[i];
            maxRightSum = max(maxRightSum, RightSum);
        }

        // 取左半边子列、右半边子列、跨越中间的子列三者的最大值
        return max(max(leftMaxSum, rightMaxSum), maxLeftSum + maxRightSum);
    }
};



int main() {
    int arr[] = {-2, 1, -3, 4, -1, 2, 1, -5, 4};
    vector<int> nums = vector<int>(arr, arr + 9);

    cout << Solution::maxSubArray(nums, 0, 8);
    return 0;
}

  • 时间复杂度: T ( n ) = 2 T ( n / 2 ) + c n = 2 [ 2 T ( n / 2 2 ) + c n / 2 ] + c n = . . . = 2 k O ( 1 ) + c k n = O ( n l o g n ) T(n)=2T(n/2)+cn=2[2T(n/2^2)+cn/2]+cn=...=2^kO(1)+ckn=O(nlogn) T(n)=2T(n/2)+cn=2[2T(n/22)+cn/2]+cn=...=2kO(1)+ckn=O(nlogn),其中 n / 2 k = 1 n/2^k=1 n/2k=1

解法4:含标志位的在线处理算法

在线处理算法:是指它可以以序列化的方式一个个的处理输入,也就是说在开始时并不需要已经知道所有的输入。“在线”的意思是指每输入一个数据就进行即时处理,在任何一个地方中止输入,算法都能正确给出当前的解。

    基于该思想,现在我们遍历该数组, 在遍历过程中, 将遍历到的元素依次累加起来, 当累加结果小于或等于0时, 说明此时的和已经对之后的最大和计算无正向帮助,为了保证最大子列和递增,因此可以抛弃之。从下一个元素开始,重新开始累加。

    ]这里的在线处理我考虑了最大和为负数的情况,用了一个布尔类型的flag变量作为检查是否全为负数的标志,因为如果序列中全为负数,最大子列和即为该数列的最大负数;反之,如果序列中存在至少一个正数,则采用“直接抛弃累加和小于0”的策略是可以计算出正确结果的

    实现代码如下:

#include <iostream>
#include <algorithm>
#include <vector>

using namespace std;

class Solution {
public:
    static int maxSubArray(vector<int> &nums) {
        int sum = 0, maxSum = -100001;

        bool negFlag = false;
        int maxNum = -100001;

        for (int num : nums) {
            maxNum = max(maxNum, num);
            if (num >= 0) {
                negFlag = true;
            }
            sum += num;
            maxSum = max(maxSum, sum);
            if (sum < 0) {
                sum = 0;
            }
        }

        if (negFlag) {
            return maxSum;
        } else {
            return maxNum;
        }
    }
};

int main() {
	// 一般情况下的序列
    // int arr[] = {-2, 1, -3, 4, -1, 2, 1, -5, 4};
	// 考虑全是负数的情况
    int arr[] = {-10, -9, -8 ,-7 ,-6, -5, -4, -3, -2};
    vector<int> nums = vector<int>(arr, arr + 9);

    cout << Solution::maxSubArray(nums);
    return 0;
}
  • 时间复杂度: O ( n ) O(n) O(n)

解法5:不含标志位的在线处理算法

    这一解法是对解法4的优化。如果不用标志位也可以,只需要改变一下对当前和为负数的处理方式即可,让result一直保留着较大的数。这样一来,即使是负数,遍历完整个vector之后就会得到最大的数,也就是最大负数。

    代码如下:

#include <iostream>
#include <algorithm>
#include <vector>

using namespace std;

class Solution {
public:
    static int maxSubArray(vector<int> &nums) {
        int maxSum = INT_MIN;
        int sum = 0;
        for (int num : nums){
            sum += num;
            maxSum = max(maxSum, sum);
            sum = max(sum, 0);
        }
        return maxSum;
    }
};

int main() {
//    int arr[] = {-2, 1, -3, 4, -1, 2, 1, -5, 4};
    int arr[] = {-10, -9, -8 ,-7 ,-6, -5, -4, -3, -2};
    vector<int> nums = vector<int>(arr, arr + 9);

    cout << Solution::maxSubArray(nums);
    return 0;
}
  • 时间复杂度: O ( n ) O(n) O(n)

解法6:动态规划

    假设 n u m s nums nums数组的长度是 n n n,下标从 0 0 0 n − 1 n−1 n1。如果用 f ( i ) f(i) f(i)代表以第 i i i个数结尾的最大和,那么我们只需要求出每个位置的 f ( i ) f(i) f(i),然后返回 f f f数组中的最大值即可。

    为了求出 f ( i ) f(i) f(i),需要进行分类讨论: n u m s [ i ] nums[i] nums[i]“自立门户”,或是加入 f ( i − 1 ) f(i−1) f(i1),哪个大我们要哪个。因此动态规划转移方程也就得到了:

f ( i ) = max ⁡ { f ( i − 1 ) + nums [ i ] , nums [ i ] } f(i) = \max \{ f(i-1) + \textit{nums}[i], \textit{nums}[i] \} f(i)=max{f(i1)+nums[i],nums[i]}

    用一个数组来保存 f ( i ) f(i) f(i)的值,用一个循环求出所有 f ( i ) f(i) f(i)。由于 f ( i ) f(i) f(i)只和 f ( i − 1 ) f(i−1) f(i1)相关,于是可以模仿“滚动数组”,只用一个变量 s u m sum sum来维护对于当前 f ( i ) f(i) f(i) f ( i − 1 ) f(i−1) f(i1)的值是多少,从而让空间复杂度降低到 O ( 1 ) O(1) O(1)

    代码如下:

#include <iostream>
#include <algorithm>
#include <vector>

using namespace std;

class Solution {
public:
    static int maxSubArray(vector<int> &nums) {
        int sum = 0, maxSum = nums[0];

        for (int num : nums) {
            sum = max(sum + num, num);
            maxSum = max(maxSum, sum);
        }
        
        return maxSum;
    }
};

int main() {
    int arr[] = {-2, 1, -3, 4, -1, 2, 1, -5, 4};
    vector<int> nums = vector<int>(arr, arr + 9);

    cout << Solution::maxSubArray(nums);
    return 0;
}
  • 时间复杂度: O ( n ) O(n) O(n)

欢迎各位读者在评论区中批评指正,如果对你有用的话,记得点赞收藏哦~

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

MomentNi

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值