求最大子段和的几种方法以及性能测试
2015-07-29 21:57
330 查看
我是计院出身的,但是感觉自己算法方面比较薄弱,这些天抽些时间把以前的问题拿出来总结一下。最大子段和问题是一个比较基础的问题,在《数据结构与算法:c语言实现》比较靠前的位置。
为了便于说明,假设存在一个数组a,长度length.a和b做下标且a小于b。
算法一
时间复杂度:O(n^3)
算法思想:对于每一个可能的区间[a,b]都去求其和,找到其中的最大值。
这个算法是最简单粗暴的一种,也的确非常好理解。
算法二
时间复杂度:O(n^2)
算法思想:和上一个算法大同小异,区别是先只确定开始的下标a。
这算法其实也很好理解哒~
算法三
时间复杂度:O(n*logn)
算法简述:使用分治法策略。将这个数组一分为二,则最大子段有三种情况
在左半部分
在右半部分
左半部分和右半部分都有
先看看代码
左半部分和右半部分的最大子段用递归求得,而左半部分和右半部分兼有的最大子段就需要用点小手段了。之后将三者进行比较,返回三者最大者。
其实最不好理解的部分就是递归的“终点”,如果当前的值大于0则返回当前值,如果小于零则返回0。这个应该怎么考虑呢?对于当前的单位而言,有两种选择,选或者不选。不选则为0。
要想理解这个算法,我觉得最重要的还是理解清楚分治的目的到底是什么。——>求出当前范围内的最大值。无论当前单元是多大,哪怕只有1(虽然特殊,但是目的没有变)。
算法四
这个算法其实是在大二的时候就已经看到过了,但是直到今日重新看起,还是觉得很秒。尤其是和前几个算法对比,不光代码精简,时间复杂度也降低到了O(n)。程序设计老师所说的好的代码是艺术的,这大概就是一份好的代码。
动态规划
时间复杂度:O(n)
算法思想:动态规划
这个方法刚看上去有些反直觉。我的第一反应就是:这真的可以得到最大子段和么?
首先我们看一看这个问题是否符合动态规划算法要求的性质:
能采用动态规划求解的问题的一般要具有3个性质:
最优化原理:如果问题的最优解所包含的子问题的解也是最优的,就称该问题具有最优子结构,即满足最优化原理。
这个是肯定的,假设有两个段,A和B且,A-B=a,则A的最大子段和就是B的最大子段和+(a或者0)。
无后效性:即某阶段状态一旦确定,就不受这个状态以后决策的影响。也就是说,某状态以后的过程不会影响以前的状态,只与当前状态有关。
如题,算法四并没有影响之前的状态。
有重叠子问题:即子问题之间是不独立的,一个子问题在下一阶段决策中可能被多次使用到。(该性质并不是动态规划适用的必要条件,但是如果没有这条性质,动态规划算法同其他算法相比就不具备优势)
每一次更新都要同maxSum进行比较。
这个问题确实可以使用动态规划的思想!
回头再看看算法四:其实每一次循环过程都是对上一次循环的拓展。
我来拙劣地表示一下
thisSum(n)=max(thisSum(n-1)+thisNum,thisNum)
调用测试的函数如下
当测试规模为1000时
当测试规模为5000时
这个时候明显看到了算法一和算法二已经很吃性能了!
当测试规模打到10000时
我去minecraft烧了一片树林才跑完,这个时候算法一已经没法看了。。
但是当测试规模达到了100000时呢?
为了便于说明,假设存在一个数组a,长度length.a和b做下标且a小于b。
算法一
时间复杂度:O(n^3)
算法思想:对于每一个可能的区间[a,b]都去求其和,找到其中的最大值。
int max_sub_sum_1(const int a[], int length){ int thisSum, maxSum = 0, i, j, k; for (i = 0; i < length; i++){ for (j = 0; j < length; j++){ thisSum = 0; for (k = i; k < j; k++){ thisSum += a[k]; } maxSum = thisSum>maxSum ? thisSum : maxSum; } } return maxSum; }
这个算法是最简单粗暴的一种,也的确非常好理解。
算法二
时间复杂度:O(n^2)
算法思想:和上一个算法大同小异,区别是先只确定开始的下标a。
int max_sub_sum_2(const int a[],int length){ int thisSum, maxSum, i, j; maxSum = 0; for (i = 0; i < length; i++){ thisSum = 0; for (j = i; j < length; j++){ thisSum += a[j]; if (thisSum>maxSum){ maxSum = thisSum; } } } return maxSum; }
这算法其实也很好理解哒~
算法三
时间复杂度:O(n*logn)
算法简述:使用分治法策略。将这个数组一分为二,则最大子段有三种情况
在左半部分
在右半部分
左半部分和右半部分都有
先看看代码
int max_sub_sum_3_part(const int a[], int left, int right){ //分治策略 //【112,2131,-123,123,-993】 //最大子序列可能的情况有三种,左半部,右半部,或者左右结合部 int leftMaxSum = 0, rightMaxSum = 0, leftBorderMaxSum = 0, rightBorderMaxSum = 0; int leftBorderSum = 0,rightBorderSum = 0; int center, i; //递归的终点应该是此“单元”的规模缩减到了1 if (left == right){ //base 到达了单个 if (a[right] > 0){ return a[right]; } else{ return 0; } } center = (left + right) / 2; leftMaxSum = max_sub_sum_3_part(a, left, center); rightMaxSum = max_sub_sum_3_part(a, center+1, right); // for (i = center+1; i<=right; i++){ rightBorderSum += a[i]; if (rightBorderSum>rightBorderMaxSum){ rightBorderMaxSum = rightBorderSum; } } for (i = center; i >=left; i--){ leftBorderSum += a[i]; if (leftBorderSum > leftBorderMaxSum){ leftBorderMaxSum = leftBorderSum; } } return max(max(leftMaxSum, rightMaxSum), leftBorderMaxSum + rightBorderMaxSum); }
//为了保持其一致性 int max_sub_sum_3(const int a[], int length){ //使用了分治法 return max_sub_sum_3_part(a, 0, length - 1); }
左半部分和右半部分的最大子段用递归求得,而左半部分和右半部分兼有的最大子段就需要用点小手段了。之后将三者进行比较,返回三者最大者。
其实最不好理解的部分就是递归的“终点”,如果当前的值大于0则返回当前值,如果小于零则返回0。这个应该怎么考虑呢?对于当前的单位而言,有两种选择,选或者不选。不选则为0。
要想理解这个算法,我觉得最重要的还是理解清楚分治的目的到底是什么。——>求出当前范围内的最大值。无论当前单元是多大,哪怕只有1(虽然特殊,但是目的没有变)。
算法四
这个算法其实是在大二的时候就已经看到过了,但是直到今日重新看起,还是觉得很秒。尤其是和前几个算法对比,不光代码精简,时间复杂度也降低到了O(n)。程序设计老师所说的好的代码是艺术的,这大概就是一份好的代码。
动态规划
时间复杂度:O(n)
算法思想:动态规划
int max_sub_sum_4(const int a[], int length){ int thisSum = 0, maxSum = 0; int i; for (i = 0; i < length; i++){ thisSum += a[i]; if (thisSum>maxSum){ maxSum = thisSum; } else if (thisSum < 0){ thisSum = 0; } } return maxSum; }
这个方法刚看上去有些反直觉。我的第一反应就是:这真的可以得到最大子段和么?
首先我们看一看这个问题是否符合动态规划算法要求的性质:
能采用动态规划求解的问题的一般要具有3个性质:
最优化原理:如果问题的最优解所包含的子问题的解也是最优的,就称该问题具有最优子结构,即满足最优化原理。
这个是肯定的,假设有两个段,A和B且,A-B=a,则A的最大子段和就是B的最大子段和+(a或者0)。
无后效性:即某阶段状态一旦确定,就不受这个状态以后决策的影响。也就是说,某状态以后的过程不会影响以前的状态,只与当前状态有关。
如题,算法四并没有影响之前的状态。
有重叠子问题:即子问题之间是不独立的,一个子问题在下一阶段决策中可能被多次使用到。(该性质并不是动态规划适用的必要条件,但是如果没有这条性质,动态规划算法同其他算法相比就不具备优势)
每一次更新都要同maxSum进行比较。
这个问题确实可以使用动态规划的思想!
回头再看看算法四:其实每一次循环过程都是对上一次循环的拓展。
我来拙劣地表示一下
thisSum(n)=max(thisSum(n-1)+thisNum,thisNum)
性能测试
为了生成足够大的数组,我写了一个方法,可以生成一组任意大小的随机数。为了方便之后的对比,还会把这组随机数写入到文件中保存。#include <time.h> #include <stdio.h> #include <random> #include <stdlib.h> #include <math.h> #define random(x) (rand()%x) /******声称测试数据**************************/ int get_test_data_f(int *data, const char* file_name, int size, int max_num){ FILE* output_file = fopen(file_name, "w+"); if (NULL == output_file){ return -1; } srand((int)time(0)); for (int i = 0; i < size; i++){ data[i] = random(max_num); fprintf(output_file, "%d\n", data[i]); } return 0; }
调用测试的函数如下
#include "_data.h" #include "stdlib.h" #include "max_sub_sum.h" #include "binary_search.h" #define SIZE 1000 #define MAX_NUM 200 int main(){ clock_t start_time, finish_time; int *a = (int*)malloc(sizeof(int)*SIZE); get_test_data_f(a, "data_intent_test.txt", SIZE, MAX_NUM); for (int i = 0; i < SIZE; i++){ printf("%d\n", a[i]); } start_time = clock(); printf("%d\n",max_sub_sum_1(a,SIZE)); finish_time = clock(); printf("算法1运行时间为:%d\n", (finish_time - start_time)); start_time = clock(); printf("%d\n", max_sub_sum_2(a, SIZE)); finish_time = clock(); printf("算法2运行时间为:%d\n", (finish_time - start_time)); start_time = clock(); printf("%d\n", max_sub_sum_3(a, SIZE)); finish_time = clock(); printf("算法3运行时间为:%d\n", (finish_time - start_time)); start_time = clock(); printf("%d\n", max_sub_sum_4(a, SIZE)); finish_time = clock(); printf("算法4运行时间为:%d\n", (finish_time - start_time)); system("pause"); return 0; }
当测试规模为1000时
当测试规模为5000时
这个时候明显看到了算法一和算法二已经很吃性能了!
当测试规模打到10000时
我去minecraft烧了一片树林才跑完,这个时候算法一已经没法看了。。
但是当测试规模达到了100000时呢?