前言
最大子列和问题是数据结构与算法分析中最经典的题目之一,其解法非常多,许多算法的精髓都能在这一问题中得到体现。本文将对该题的不同解法做一个汇总,同时增加了最大值为负数时的情况,方便自己日后复习。这篇文章如果对你有用的话记得一键三连哦!!
原题重现
-
通俗表达:给定一个整数数组nums( 1 ≤ n u m s . l e n g t h ≤ 3 ∗ 1 0 4 1 \le nums.length \le 3 * 10^4 1≤nums.length≤3∗104且 − 1 0 5 ≤ n u m s [ i ] ≤ 1 0 5 -10^5 \le nums[i] \le 10^5 −105≤nums[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(i≤j)的最大值。
解法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 n−1。如果用 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(i−1),哪个大我们要哪个。因此动态规划转移方程也就得到了:
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(i−1)+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(i−1)相关,于是可以模仿“滚动数组”,只用一个变量 s u m sum sum来维护对于当前 f ( i ) f(i) f(i)和 f ( i − 1 ) f(i−1) f(i−1)的值是多少,从而让空间复杂度降低到 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)
欢迎各位读者在评论区中批评指正,如果对你有用的话,记得点赞收藏哦~