问题描述
一个整数数组中的元素有正有负,在该数组中找出一 个连续子数组,要求该连续子数组中各元素的和最大,这个连续子数组便被称作最大连续子数组。比如数组{2,4,-7,5,2,-1,2,-4,3}的最大连续子数组为{5,2,-1,2},最大连续子数组的和为5+2-1+2=8。问题输入就是一个数组,输出该数组的“连续子数组的最大和”。
五种解法
(1)暴力算法 三层循环,O(n^3)级别
//2, 4, -7, 5, 2, -1, 2, -4, 3
//改良版1 暴力模拟,三层循环,O(n^3)级别
//任何一个子数组都能遍历到,都判断一次,即便是只有一个。
int MaxLong1(int a[], int len)
{
//比较MaxLong2,仅仅是加了一层控制,而这个控制可以不要
int i, j, k;
int Maxnum = 0;
int Curnum = 0;
for (i = 0; i < len; i++) //确保每个数字都能遍历到
{
for (j = i; j < len; j++) //控制某子数组内1内
{
Curnum = 0;
for (k = i; k <= j; k++) //控制某子数组内1内的某子数组2内,寻找最大值
{
Curnum += a[k];
if (Curnum>Maxnum)
{
Maxnum = Curnum;
}
}
}
}
return Maxnum;
}
(2)在(1)暴力算法基础上撤出一个for循环,O(N^2)级别
//改良版2 在改良版1基础上撤出一个for循环,O(N^2)级别
//2, 4, -7, 5, 2, -1, 2, -4, 3
//复杂度 O(N*2)
//任何一个子数组都能遍历到,都判断一次,即便是只有一个。
int MaxLong2(int a[], int len)
{
//比较MaxLong1,仅仅是省去了一层控制----第三个for,
//而这个控制可以不要,可以根据for循环的特性去掉,因为for循环本来就是这样子下去的,这样就重复计算了前面计算的,增加了复杂度
int i, j;
int Maxnum = 0;
int Curnum = 0;
for (i = 0; i < len; i++) //控制每个数字都能遍历到
{
Curnum = 0;
for (j = i; j < len; j++) //控制在子数组1内,同时,还能控制子数组1内任意连续子数组2内都能判定一次
//利用的是for循环++的特性。
{
Curnum += a[j];
if (Curnum > Maxnum)
{
Maxnum = Curnum;
}
}
}
return Maxnum;
}
(3)分治法 次优算法,采用分治策略 时间复杂度T(n)= O(nlogn)
因为最大子序列和可能在三处出现,整个出现在数组左半部,或者整个出现在右半部,又或者跨越中间,占据左右两半部分(向两边搜索前进)。递归将左右子数组再分别分成两个数组,直到子数组中只含有一个元素,退出每层递归前,返回上面三种情况中的最大值。
int Divide(int a[], int left, int right)
{
int leftMaxSum, rightMaxSum; //左边最大值 右边最大值
int midleftMaxSum,midrightMaxSum; //中间的左边最大值,中间的右边最大值
int midleft , midright ; //左边中间项,右边中间项
if (left == right)
{
if (a[left] > 0)
{
return a[left];
}
else
{
return 0;
}
}
int center = (left + right) / 2;
leftMaxSum = Divide(a, left, center); //左边 //这里不能-1,因为(left + right) / 2向下取证整了
//例如 只剩两个数,center = 0,center -1 = -1,就会报错了;
rightMaxSum = Divide(a, center + 1, right); //右边 //这里加一不会出错,因为已经向下取证了
//中间
//中间的左边
midleftMaxSum = 0;
midleft = 0;
for (int i = center; i >= left; i--)
{
midleft += a[i];
if (midleft > midleftMaxSum)
{
midleftMaxSum = midleft;
}
}
//中间的右边
midrightMaxSum = 0;
midright = 0;
for (int i = center+1; i <= right; i++)
{
midright += a[i];
if (midright > midrightMaxSum)
{
midrightMaxSum = midright;
}
}
int midmax = midleftMaxSum + midrightMaxSum; //中间最大值
//比较出最大值
int max = leftMaxSum > rightMaxSum ? leftMaxSum : rightMaxSum;
return max > midmax ? max : midmax;
}
(4)动态规划(DP) 复杂度为 O(n)。
步骤 1:令状态 dp[i] 表示以 A[i] 作为末尾的连续序列的最大和(这里是说 A[i] 必须作为连续序列的末尾)。
步骤 2:做如下考虑:因为 dp[i] 要求是必须以 A[i] 结尾的连续序列,那么只有两种情况:
-
- 这个最大和的连续序列只有一个元素,即以 A[i] 开始,以 A[i] 结尾。---》(DP[i-1]小于0)
- 这个最大和的连续序列有多个元素,即从前面某处 A[p] 开始 (p<i),一直到 A[i] 结尾。 --》(DP[i-1]>0)
对第一种情况,最大和就是 A[i] 本身。
对第二种情况,最大和是 dp[i-1]+A[i]。
于是得到状态转移方程:
dp[i] = max{A[i], dp[i-1]+A[i]}
这个式子只和 i 与 i 之前的元素有关,且边界为 dp[0] = A[0],由此从小到大枚举 i,即可得到整个 dp 数组。接着输出 dp[0],dp[1],...,dp[n-1] 中的最大子即为最大连续子序列的和。
#include <iostream>
using namespace std;
#include <algorithm>
#include <functional>
//动态规划
int max(int a, int b)
{
return a > b ? a : b;
}
// dp[i] = max{A[i], dp[i-1]+A[i]}
void process(int a[], int dp[], int size) //求出各个节点为结尾的最大值
{
for (int i = 1; i < size; i++)
{
dp[i] = max(a[i], a[i] + dp[i - 1]);
}
}
void main()
{
int a[] = { 2, 4, -7, 5, 2, -1, 2, -4, 3 };
int size = sizeof(a) / sizeof(int);
int DP[100] = { 0 }; //假设100够用了
process(a, DP, size);
sort(DP, DP + size, greater<>()); //直接打乱,反正也不用了,当然可以if判断,我懒一点
cout << "最大连续子序列: " << DP[0] << endl;
system("pause");
}
(5)线性时间算法 时间复杂度O(n)
该算法在每次元素累加和小于0时,从下一个元素重新开始累加。实现代码如下:
//最优方法,时间复杂度O(n)
int line(int a[], int size)
{
int Maxnum = 0;
int Curnum = 0;
for (int i = 0; i < size; i++)
{
Curnum += a[i];
if (Curnum > Maxnum)
{
Maxnum = Curnum;
}
//如果累加和出现小于0的情况,
//则和最大的子序列肯定不可能包含前面的元素,
//这时将累加和置0,从下个元素重新开始累加
else if (Curnum < 0) //为什么加上else ,因为if (Curnum > Maxnum)成立时,Curnum肯定大于0,没必要判断
{
Curnum = 0;
}
}
return Maxnum;
}
(4)(5)两种算法应该是一种。
参考 https://www.cnblogs.com/allzy/p/5162815.html
https://www.cnblogs.com/coderJiebao/p/Algorithmofnotes27.html