共读《计算之魂》-吴军
例题1.3:总和最大区间的四种方法对比
写在前面
大佬们看到这篇小文章肯定会吐槽,这小子写程序怎么只会用for循环啊…😅
最近一直在读吴军老师的《计算之魂》,在学着去写递归函数。但是能力有限,思维开始一点点的调整过来,但是书写上还需要点时间练习,打磨技术。脱离了递归思想的支撑(其实也就脱离了计算机的灵魂),我就用最基本的程序设计方法做的咱们书上的这道思考题目。虽然没用用上递归,但是我力求把程序书写思路写清楚,增加其可读性。
这也符合了本书吴军老师的写作思路,因为递归
的思想在下一章才会进行详细的讲解。
后面当我递归函数书写熟练以后,我也一定会回来去修改这些代码的!
题目描述
对于给定是实数序列,设计一个算法,找到一个总和最大的区间。
书中吴老师给出的序列为:float nums[13] = {1.5,-12.3,3.2,-5.5,23.2,3.2,-1.4,-12.2,34.2,5.4,-7.8,1.1,-4.9}
特殊情况:
如果数组中只有一个正数,那么这个正数对应的索引区间就是总和最大区间。
方法1
方法1就是利用三层for循环,利用排列组合的思想:头指针从nums[0]开始到nums[12]把数组扫一遍,尾指针从头指针的位置开始到nums[12];组合方式0(K2)种,在每一种组合中平均要做K/4次求和运算。
优点:书写思路直观、较简洁
缺点:做了太多的无用功。
无用功从何而来呢?
假设区间的起点是nums[0],终点是nums[k] (0 < k <13),使用方法1在计算sum(0,k+1)时,会从头开始计算,而不是在已有sum[0,k]的结果之上加上nums[k + 1],所以会产生大量的无用用功。数据量小无所谓,但是当数据量提升量级的时候,运算时间上就会逐渐产生极大地差别。
//这个代码一开始把sum的位置放在了三个foe循环的外面
//调试发现:造成每次进入第三个for求sum时,sum仍然保留着前面结果
//以至于结果出错
#include<stdio.h>
//最大值函数
float max(float a , float b){
return a > b ? a : b;
}
//主函数
int main(void){
float ans = 0.0;//结果变量
//找出总和最大的区间
float nums[13] = {1.5,-12.3,3.2,-5.5,23.2,3.2,-1.4,-12.2,34.2,5.4,-7.8,1.1,-4.9};//题目中已知的数组
for(int i = 0;i < 13;i++){
for(int j = i;j < 13;j++){
//计算区间[i,j]的和
float sum = 0.0;//数组求和结果
for(int m = i;m <= j;m++){
sum += nums[m];
}
ans = max(ans,sum);
}
}
printf("the result is:%f",ans);
return 0;
}
the result is: 52.400005
Process exited after 0.01438 seconds with return value 0
方法2
折腾了两天了,始终不明白第二问咋想、咋做。倒是先把第四问做出来了…
反复重读书中这句话(P38):在方法2中,我们先假设区间的左边界p,再次确定的条件下确定综合最大区间的右边界q。
好啦,运用方法2解决这个问题的第一步就是,我怎么去找这个右边界啊?😩
吴军老师在书中解释到的需要记录的三个值:
- 从p开始到当前位置q为止的总和sum(p,q)
- 从p开始到当前位置q为止所有和中的最大值Maxsum
- 区间的结束位置
那么好了,p就是从头开始正向扫描,扫描一遍得到最大值和最大值对应的索引下标。因为max = sum(p,q) > sum(p,q+1)当q增大时恒成立,也就是说,最大值索引下标之后无论数组有多长,加上那个nums[p+1]后,总会使sum(p,q+1)小于max=sum(p,q)。
序号 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|
元素 | 1.5 | -12.3 | 3.2 | -5.5 | 23.2 | 3.2 | -1.4 | -12.2 | 34.2 | 5.4 | -7.8 | 1.1 | -4.9 |
向前累加和 | 1.5 | -10.8 | -7.6 | -13.1 | 10.1 | 13.3 | 11.9 | -0.3 | 33.9 | 39.3 | 31.5 | 32.6 | 27.7 |
#include<stdio.h>
//最大值函数
float max(float a , float b){
return a > b ? a : b;
}
int main(void){
//先去把有边界确定好
float Max = 0.0;
float nums[13] = {1.5,-12.3,3.2,-5.5,23.2,3.2,-1.4,-12.2,34.2,5.4,-7.8,1.1,-4.9};//题目中已知的数组
int left,right = 0;//左、右边界确定指针
float ans = 0.0;
float anw[13];
//求正向累加序列
for(int i = 0;i < 13;i++){
float sum = 0.0;
for(int j = 0;j <= i;j++){
sum += nums[j];
}
anw[i] = sum;
}
//确定右边界
for(int i = 0;i < 13;i++){
ans = max(ans,anw[i]);
}
for(int i = 0;i < 13;i++){
if(anw[i] == ans){
right = i;
}
}
//注意右边界为区间右端+1
for(int i = 0;i < right + 1;i++){
float sum = 0.0;
for(left = i;left < right + 1;left++){
sum += nums[left];
}
Max = max(sum,Max);
}
printf("%f",Max);
return 0;
}
52.400005
Process exited after 0.0121 seconds with return value 0
方法3
分治思路:
将序列分为:1-K/2和K/2-K两部分。由于这个问题中数组中含有13个奇数元素,所以我直接从第七个元素开始划分左右子序列。由于现在我递归函数写的不好,所以整个过程都是用for循环实现的,代码冗余度较高,空间复杂度大,但是思路还是顺畅的,场上到下一气呵成。第三问、第四问总结起来都是从第二问的延伸扩展。第三问只不过把问题的尺度缩小了,将一个长序列转换为分隔成为了两个子序列,所以会写递归就会节省很大的空间复杂度,目前写不出递归,那么就要消耗空间。因为啥?因为子序列的处理和一个长序列的处理步骤、方法是一致的。比如,一个长序列,写两个for循环就解决了;但是现在你分成了两个子序列,所以连个for循环就要用两遍…
分治的思想
一定是要掌握的,特别是在递归函数熟练书写之后,再次反刍这道题,一定会有不一样的收获。🌹
#include<stdio.h>
//两个数的最大值函数
float max(float a , float b){
return a > b ? a : b;
}
//三个数的最大值函数
float maxthree(float a , float b, float c){
//三目简洁运算,比较得到三个数的最大值
return a > b ? a > c ? a : c : b > c ? b : c;
}
//分治思想
int main(void){
//先去把有边界确定好
float Max1 = 0.0;
float Max2 = 0.0;
float nums[13] = {1.5,-12.3,3.2,-5.5,23.2,3.2,-1.4,-12.2,34.2,5.4,-7.8,1.1,-4.9};//题目中已知的数组
int left1,right1 = 0;//左子序列的左、右边界确定指针
int left2,right2 = 0;//右子序列的左、右边界确定指针
float ans1 = 0.0;//左子序列的最大值
float ans2 = 0.0;//右子序列的最大值
float anw1[13];//左子序列的最大值
float anw2[13];//左子序列的最大值
//求右部分正向累加序列
for(int i = 0;i < 7;i++){
float sum = 0.0;
for(int j = 0;j <= i;j++){
sum += nums[j];
}
anw1[i] = sum;
}
//确定右部分的右边界
for(int i = 0;i < 7;i++){
ans1 = max(ans1,anw1[i]);
}
for(int i = 0;i < 7;i++){
if(anw1[i] == ans1){
right1 = i;
}
}
//注意右边界为区间右端+1
for(int i = 0;i < right1 + 1;i++){
float sum = 0.0;
for(left1 = i;left1 < right1 + 1;left1++){
sum += nums[left1];
}
Max1 = max(sum,Max1);
}
//-------------------------------------------------------------
//求左部分正向累加序列
for(int i = right1;i < 13;i++){
float sum = 0.0;
for(int j = 0;j <= i;j++){
sum += nums[j];
}
anw2[i] = sum;
}
//确定右部分的右边界
for(int i = right1;i < 13;i++){
ans2 = max(ans2,anw2[i]);
}
for(int i = right1;i < 13;i++){
if(anw2[i] == ans2){
right2 = i;
}
}
//注意右边界为区间右端+1
for(int i = right1;i < right2 + 1;i++){
float sum = 0.0;
for(left2 = i;left2 < right2 + 1;left2++){
sum += nums[left2];
}
Max2 = max(sum,Max2);
}
//分别找出左子序列[p1,q1]最大值/右子序列[p2,q2]/整个区间[p1,q2]三者的最大值
float Max3 = 0.0;
//求和索引期间为[right1 - 1,right2 + 1]
for(int i = 4;i < 10;i++){
Max3 += nums[i];
}
//-----------------------------------------------------------------
float maxsum = maxthree(Max1,Max2,Max3);
printf("%d\n",right1);
printf("%d\n",right2);
printf("the final result is %f\n",maxsum);
return 0;
}
the final result is 52.400005
Process exited after 0.01318 seconds with return value 0
方法4
正反双向扫描->逆向思维
首先吐槽一下自己写的代码,虽然结果运行没错,但是我认为求最值问题可以转化为滑动窗口或者动态规划;现阶段本人水平有限,一直也在思考新的思路,也恳请各位大佬将自己的思路share,已改进下面的代码。
一直感觉这种方法可以用动态规划的思路去解决,但是由于动态规划的思路目前还没有熟练掌握,所以编写过程还是停留在了运用for循环的方式上…
反向扫描去找到左节点是解决本题的关键所在,这也是需要学习的思想->逆向思维。
#include<stdio.h>
//最大值函数
float max(float a , float b){
return a > b ? a : b;
}
//主函数
int main(void){
//找出总和最大的区间
float nums[13] = {1.5,-12.3,3.2,-5.5,23.2,3.2,-1.4,-12.2,34.2,5.4,-7.8,1.1,-4.9};//题目中已知的数组
int left = 0,right = 0;//左右指针确定最终区间
float res = 0.0;//最终结果
float ans = 0.0;//结果变量
float ans1 = 0.0,ans2 = 0.0;//双向扫描后的最大值
float anw1[13];//正向扫描累加序列
float anw2[13];//反向扫描累加序列
//求正向累加序列
for(int i = 0;i < 13;i++){
float sum = 0.0;
for(int j = 0;j <= i;j++){
sum += nums[j];
}
anw1[i] = sum;
}
//求反向累加序列
for(int i = 12;i >= 0 ;i--){
float sum = 0.0;
for(int j = 13;j >= i;j--){
sum += nums[j];
}
anw2[i] = sum;
}
//确定右边界
for(int i = 0;i < 13;i++){
ans1 = max(ans1,anw1[i]);
}
for(int i = 0;i < 13;i++){
if(anw1[i] == ans1){
right = i;
}
}
//确定左边界
for(int i = 0;i < 13;i++){
ans2 = max(ans2,anw2[i]);
}
for(int j = 0;j < 13;j++){
if(anw2[j] == ans2){
left = j;
}
}
//求最终结果
for(int m = left;m <= right;m++){
res += nums[m];
}
printf("the final result is =%f",res);
return 0;
}
the final result is =52.400005
Process exited after 0.009718 seconds with return value 0
不会用递归,好的方法代码越写越长…
所以,路漫漫其修远兮,吾将上下而求索!
封面照片引:微博:@铁憨憨nangesfg——《2098》大国朋克系列;本人最爱的画集之一