计算之魂读书笔记

共读《计算之魂》-吴军

——读书笔记与练习思考心shui™总结©杨桂淼
例题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)。

序号12345678910111213
元素1.5-12.33.2-5.523.23.2-1.4-12.234.25.4-7.81.1-4.9
向前累加和1.5-10.8-7.6-13.110.113.311.9-0.333.939.331.532.627.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》大国朋克系列;本人最爱的画集之一

  • 4
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

mungeryang

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值