最大连续子数组
前言
最大连续子数组问题作为面试题的经典问题,在算法导论书中给出了几中求解方式,由于最近在复习算法方面知识,遂通过描述用C语言对内容进行总结。
算法介绍
给定一个数组A[0,1,...,n-1],其数组元素有正有负,求取其中连续子序列的和最大。
例如:
求取算法介绍
而针对该问题的求取方法,通常有4中常用的算法:暴力法,分治法,分析法,动态规划法。
从前到后时间复杂度分别为,
,
,
算法详解
一、暴力法
进行3次循环:
一、循环i确定起始位置
二、循环j确定终止位置
三、循环k求和
代码如下:
void violence(int *A, int n) {
// 设置初始最大值
int maxSum = A[0];
int currentSum;
// 最大子数组起始位置
int maxStart = 0;
// 最大子数组终止位置
int maxEnd = 0;
// 三层循环求取最大值
for (int i = 0; i < n; i++) {
for (int j = i; j < n; j++) {
currentSum = 0;
for (int k = i; k < j; k++) {
currentSum += A[k];
}
if (currentSum > maxSum) {
maxSum = currentSum;
maxStart = i;
maxEnd = j;
}
}
}
for (int i = maxStart; i < maxEnd; i++) {
printf("A%d]=%d\n", i, A[i]);
}
}
二、分治法
将数组从中间分开,那么最大子数组要么完全在左半边数组,要么完全在右半边数组,要么跨立在分界点上。
完全在左数组、右数组递归解决。
跨立在分界点上:实际上是左数组的最大后缀和右数组的最大前缀的和。因此,从分界点向前扫,向后扫即可。
代码如下:
int devideCode(int *A, int from, int to) {
// 所引到最后2个元素就直接输出
if (to == from) {
return A[from];
}
// 获取中点位置
int middle = (from + to) / 2;
// 递归获取每个分块后子序列的最长子序列和
int m1 = devideCode(A, from, middle);
int m2 = devideCode(A, middle + 1, to);
int i;
int left = A[middle];
int now = A[middle];
// 求取向左扫最大值
for (i = middle - 1; i >= from; --i) {
now += A[i];
left = maxTwo(now, left);
}
int right = A[middle + 1];
now = A[middle + 1];
// 求取向右扫最大值
for (i = middle + 2; i <= to; ++i) {
now += A[i];
right = maxTwo(now, right);
}
// 对左右扫求和
int m3 = left + right;
// 比较左侧值右侧值及左右扫求和值大小,找到最大子序列长度的值
return maxThree(m1, m2, m3);
}
三、分析法
1、求取前缀和p[i]
p[i] = a[0] + a[1] + ...+a[i]
遍历i:0≤i ≤n-1
p[i]=p[i-1]+A[i]
2、计算p[i]-p[j]
遍历i:0≤i ≤n-1,求p[i]的最小值m
找到最小值后p[i]-m即为以a[i]结尾的数组中最大的子数组
代码如下:
void analyze(int *A, int n) {
// p[i]=A[0]+...A[n-1]按照索引求和数组
int *p;
// 最长数组起始和终止位置
int startFlag = 0;
int endFlag = 0;
// 初始化
p = (int*)malloc(n * sizeof(int));
for (int i = 0; i < n; i++) {
p[i] = 0;
}
// step 1 获取p[i]
p[0] = A[0];
for (int i = 1; i < n; i++) {
p[i] = p[i - 1] + A[i];
}
// step 2 求取p[i]最小值及求取最长子序列并记录位置
int min = p[0];
for (int i = 1; i < n; i++) {
if (p[i] < min) {
min = p[i];
startFlag = i;
}
}
int max = min;
for (int i = startFlag + 1; i < n; i++) {
if (max < p[i] - min) {
max = p[i] - min;
endFlag = i;
}
}
for (int i = startFlag + 1; i < endFlag + 1; i++) {
printf("A[%d]=%d\n", i, A[i]);
}
}
四、动态规划
由于求取最大子序列过程中,某一部分求和值如果为负数那么这部分求和就不可取。
记S[i]为以A[i]结尾的数组中和最大的子数组。
则递推公式可以计做:S[i+1] = max(S[i]+A[i+1], A[i+1])。
如果判断前面求和的值加上当前值小于当前值那么相当于前面的值是使序列求和变小的项,因此可以舍去前面的项。
之后再比较舍去前面某些项之后的最大值即可求出最终最长序列了。
代码如下:
void dynamic(int *A, int n) {
// 最终最大值变量
int result = A[0];
// 递推公式值
int sum = A[0];
// 最大子序列起始位置和终止位置
int start = 0;
int end = 0;
for (int i = 1; i < n; i++) {
// 求取递推公式过程
if (sum > 0) {
sum += A[i];
}
else {
sum = A[i];
start = i;
}
// 去掉最小部分之后的最大值
if (sum > result) {
result = sum;
end = i + 1;
}
}
for (int i = start; i < end; i++) {
printf("A[%d]=%d\n", i, A[i]);
}
printf("result=%d\n", result);
}
参考资料:
余祥宣等,计算机算法基础[M],华中科技大学出版社,2001