1.问题的定义
顾名思义,最大子数组问题是求一个数组Array中“和最大的非空连续子数组“,这样的连续子数组我们叫做最大子数组,它的应用也有
很多,比如说找出时间序列中两个时间节点使得这两个时间节点对应的值的落差最大,如下图:
2.问题的求解
2.1暴力求解
这是最容易想到的办法,对于规模为n的问题,需要尝试C(2,n)次(代表组合数,这里不方便打公式),因为每次的处理时间也至少是常量,因此,这种方法的运行的时间下界是n的平方,当n很大的时候,这种方法不可取。下面先介绍一种时间复杂度上界为n平方的分治的递归算法,最后再分析一种非递归的线性算法。2.2分治算法
这里我们是求“一个最大子数组”,而不是求“最大子数组”,因为最大子数组可能有多个,另外,很显然只有数组中有负数的时候问题才有意义。分治算法的关键是问题如何划分为两个(或多个)子问题,之后又是如何进行合并,划分问题成为两个规模更小的子问题的分析如下:
找到数组Array的中央位置mid,然后考虑求解Array[low,...,mid]和Array[mid+1,...,high],而Array[low,...,high]的任何连续子数组Array[i,...,j]所处的位置必然是以下三种情况之一:
(1)完全位于子数组Array[low,...,mid]中,因此low=<i=<j=<mid;
(2)完全位于子数组Array[mid+1,...,high]中,因此mid<i=<j=<high;
(3)跨越中点,因此low=<i=<mid<j=<high;
因此数组Array[low,...,high]的一个最大子数组所处的位置必然是这三种的一种,于是我们可以递归求解Array[low,...,mid]各Array[mid+1,...,high]的最大子数组,因为这两个子问题仍然是最大子数组的问题,只是规模变小了,接着我们再求跨越中点的最大子数组,结果取三者中最大的。
现在我们先求跨越中点的最大子数组问题,这个问题并不是规模更小的问题,因为它要求子数组必须跨越中点,我们只需要求出形如
Array[i,...,mid]的最大子数组和形如Array[mid+1,...,j]的最大子数组,合并即可,对于这个子程序,我们返回两端的索引和最大子数组
的和(后一个子程序也返回这种形式的结果),我们定义返回结果为结构体类型:
<span style="font-size:14px;">//定义类型为结构体类型的返回结果
struct value
{
int index_left;
int index_right;
int sum;
};</span>
接下来是求跨越中点的最大子数组子程序:
//最大子数组跨越中点的情况
struct value find_max_crossing_subarray(int arr[],int low,int mid,int high)
{
struct value result;
int left_sum = MIN,right_sum = MIN;
int sum;
int max_left,max_right;
//找到左边数组的最大子数组
sum = 0;
int i;
for(i = mid;i > low-1;i--)
{
sum += arr[i];
if(sum > left_sum)
{
left_sum = sum;
max_left = i;
}
}
//找到右边数组的最大子数组
sum = 0;
for(i = mid+1;i < high+1;i++)
{
sum += arr[i];
if(sum > right_sum)
{
right_sum = sum;
max_right = i;
}
}
result.index_left = max_left;
result.index_right = max_right;
result.sum = left_sum + right_sum;
return result;
}
很显然,这个子程序的时间复杂度为线性,接下来是求数组Array最大子数组的递归程序:
//求解最大子数组问题的递归子程序
struct value find_maximum_subarray(int arr[],int low,int high)
{
//处理一般情况
if(low == high)
{
struct value result;
result.index_left = low;
result.index_right = high;
result.sum = arr[low];
return result;
}
else
{
//进行问题的划分并递归求解
int mid = (low + high)/2;
struct value left_result = find_maximum_subarray(arr,low,mid);
struct value right_result = find_maximum_subarray(arr,mid+1,high);
struct value cross_result = find_max_crossing_subarray(arr,low,mid,high);
if(left_result.sum > right_result.sum && left_result.sum > cross_result.sum)
return left_result;
else if(right_result.sum > left_result.sum && right_result.sum > cross_result.sum)
return right_result;
else
return cross_result;
}
}
这段程序很简单,处理特殊情况和一般情况,递归调用自身,之后再合并解,我们可以想象得到这是一个树形的结构,从最底层向上
合并结果,最上面即是最后的结果。
完整的程序:
#include<stdio.h>
#include<time.h>
#include<stdlib.h>
#define MAX 8888
#define MIN -8888
//最大子数组的分治求解算法
//定义类型为结构体类型的返回结果
struct value
{
int index_left;
int index_right;
int sum;
};
//最大子数组跨越中点的情况
struct value find_max_crossing_subarray(int arr[],int low,int mid,int high)
{
struct value result;
int left_sum = MIN,right_sum = MIN;
int sum;
int max_left,max_right;
//找到左边数组的最大子数组
sum = 0;
int i;
for(i = mid;i > low-1;i--)
{
sum += arr[i];
if(sum > left_sum)
{
left_sum = sum;
max_left = i;
}
}
//找到右边数组的最大子数组
sum = 0;
for(i = mid+1;i < high+1;i++)
{
sum += arr[i];
if(sum > right_sum)
{
right_sum = sum;
max_right = i;
}
}
result.index_left = max_left;
result.index_right = max_right;
result.sum = left_sum + right_sum;
return result;
}
//求解最大子数组问题的递归子程序
struct value find_maximum_subarray(int arr[],int low,int high)
{
//处理一般情况
if(low == high)
{
struct value result;
result.index_left = low;
result.index_right = high;
result.sum = arr[low];
return result;
}
else
{
//进行问题的划分并递归求解
int mid = (low + high)/2;
struct value left_result = find_maximum_subarray(arr,low,mid);
struct value right_result = find_maximum_subarray(arr,mid+1,high);
struct value cross_result = find_max_crossing_subarray(arr,low,mid,high);
if(left_result.sum > right_result.sum && left_result.sum > cross_result.sum)
return left_result;
else if(right_result.sum > left_result.sum && right_result.sum > cross_result.sum)
return right_result;
else
return cross_result;
}
}
//主程序
void main(void)
{
//设置随即种子
srand((unsigned)time(NULL));
//生成测试数组
int n = 20;
int arr[n];
int i;
printf("原始数组为:\n");
for(i = 0;i<n;i++)
{
arr[i] = rand()%100 - 50;
printf("%4d",arr[i]);
}
printf("\n");
//求得最大子数组
struct value res;
res = find_maximum_subarray(arr,0,n-1);
//打印结果
printf("最大子数组的两端下标以及子数组和分别为:%4d%4d%4d\n",res.index_left,res.index_right,res.sum);
}
运行结果如下:
再一次:
2.3非递归的增量算法
对于求解最大子数组问题,还有另一种思路,这是一种非递归的求解算法,求解过程中,处理的部分越来越长,未处
理的部分越来越短,当数组全部处理完之后,整个数组的最大子数组也就找到了。
我们可以从数组的左边界开始,从左至右处理,记录到目前为止已经处理过的最大子数组,假设已知arr[0,...,i]
的最大子数组,那么我们可以在线性时间内求出arr[0,...,i+1]的最大子数组:arr[0,...,i+1]的最大子数组只有两
种情况,它要么是arr[0,...,i]的最大子数组,要么是某个数组arr[j,...,i+1],其中0=<j=<i+1,这样在已知
arr[0,...,i]的最大子数组情况下,就可以在线性时间内求出arr[0,...,i+1]的最大子数组,i自0到n-1,那么,求解
也就完成了。
对应的C程序如下:
#include<stdio.h>
#include<stdlib.h>
#include<time.h>
#define MIN -8888
//求解最大子数组问题的非递归算法
//存放结果的结构体
struct value
{
int index_left;
int index_right;
int sum;
};
void main(void)
{
//生成测试数组
int n = 20;
int arr[n];
int i,j;
srand((unsigned)time(NULL));
printf("原始数组为:\n");
for(i = 0;i < n;i++)
{
arr[i] = rand()%100 - 50;
printf("%4d",arr[i]);
}
printf("\n");
//当前最优结果
struct value current_best;
for(i = 0;i < n-1;i++)
{
//i = 0时赋值给当前最大数组
if(i==0)
{
current_best.index_left = 0;
current_best.index_right = 0;
current_best.sum = 0;
}
int temp = 0,sum = 0,current_left;
//计算arr[0,...,i+1]的形如arr[j,...,i+1]的最大子数组,然后和之前的arr[0,...,i]的最大子数组(current_best)比较,更新当前最大子数组
for(j = i+1;j > -1;j--)
{
temp += arr[j];
if(temp > sum)
{
sum = temp;
current_left = j;
}
}
//此时,找到了arr[0,...,i+1]的形如arr[j,...,i+1]的最大子数组,现在进行比较
if(sum > current_best.sum)
{
//更新当前最优解
current_best.index_left = current_left;
current_best.index_right = i+1;
current_best.sum = sum;
}
}
//打印结果
printf("数组的最大子数组已经找到,两端的下标值分别为%4d,%4d,最大子数组的值为%4d\n",current_best.index_left,current_best.index_right,current_best.sum);
}
结果如下: