习题4-2 求幂级数展开的部分和 (20 分)-程序运行时间过长大坑问题

Source

题目:已知函数ex可以展开为幂级数1+x+x^2/2!+x^3/3!+⋯+x^/k!+⋯。现给定一个实数x,要求利用此幂级数部分和求ex的近似值,求和一直继续到最后一项的绝对值小于0.00001。

输入格式:

输入在一行中给出一个实数x∈[0,5]。

输出格式:

在一行中输出满足条件的幂级数部分和,保留小数点后四位。

输入样例:

1.2

 输出样例:

3.3201

 

综述   

     本题初看不算难,思路也很容易找到,很容易将代码大致框架敲出来,并且运行成功 ,但是这个程序在阶乘这个地方非常容易出现问题,大多数同学应该能想到时间复杂度这种问题,但是这个题还有另外一个很难被发现的坑-关于类型的范围,本次博客,我将从思路,编写,以及发现问题解决问题的流程分享一下这个有意思的PTA题目。

思路:

幂级数的第一项相当于x的0次方/0的阶乘,第二项是x的1次方/1的阶乘,我们把每一项当作一个整体程序中用n表示,用可变变量i作为循环变量,n=pow(x, (double)i)/fac(i),其中fac(i)表示i的阶乘,整体求和用循环结构解决,又题目要求最后一项的绝对值小于0.00001,这样思路就明确了,可以设置循环条件,使用while()循环,当n的绝对值大于等于0.00001时,进入循环。

实现: 

        最开始写的程序为程序1,函数主体按构思思路写的,阶乘函数用最容易想到的for()循环体实现,程序运行成功,输入测试用例,结果如预期所示,输入1.2结果为3.3201。

        但是这个程序在上传PTA检测时出现一个错误,程序显示运行时间过长,于是我换了一组数据测试,我用了一下5这个值,输入之后,控制台光标一直闪烁,但是不出结果。程序运行时间果然是很长的。

        我验证完之后倒是没多想别的,以为是函数有问题,所以直接又写了个另外一个常见的阶乘写法,采用递归的方式实现,具体代码如程序2。 

程序1:

#include <stdio.h>
#include <math.h>
int  fac(int i)
{
	int fator = 1;
	int j = 0;
	for (j = 1; j <= i; j++)
	{
		fator = fator * j;
	}
	return fator;
}
int main()
{
	double x = 0.0;
	double n = 1.0;
	int k = 0;
	double sum = 0.0;
	scanf("%lf", &x);
	int i = 0;
	while (fabs(n) >= 0.00001)
	{
		n = pow(x, (double)i)/fac(i);
		sum = sum + n;
		i++;
	}
	printf("%.4lf", sum);
	return 0;
}

        程序2是采用递归写法,阶乘的递归很容易理解,也很容易写出来,写出来程序2之后我运行程序,发现和程序1 的错误是一样的,同样是在输入数较大时程序就不动了,好像是算不出来的感觉,到这我突然意识到了我程序的问题,递归大家都知道一个常见的问题,就是复杂度较高,会来回算很多次,浪费电脑CPU资源,当我输入的数大时,为了满足最后一项的绝对值小于0.00001,循环变量i就会相应的增大,i增大,算的阶乘就会很多,但是i是依次递增的。

        这时会出现一个问题,举个例子说明,比如i=5时,是算5的阶乘,当i=6时算6的阶乘 ,算6的阶乘的时候会重复算5的阶乘,但是明明可以算6的阶乘的时候直接用5的阶乘乘6,计算机这点可是太笨了,还要自己写程序改正,如果这点不注意的话,随着项数的增加,会白白的多算很多的数,非常浪费计算机CPU,结果可想而知--算的特别慢。

程序2 

#include<stdio.h>
#include <math.h>

int  fac(int i)
{
	if (i == 0)
	{
		return 1;
	}
	else
	{
		return fac(i - 1)*i;
	}
}

 

        知道这一点后便可以继续为程序优化,相信很多同学也知道这个错误要怎么修改,这时候最好玩的地方就出现了,你会发现修改之后,输入较大的数依然会出现程序运行时间过长问题,这到底是怎么回事呢?当然这个地方一会再说。先谈谈如何改正阶乘时每一项都重复算数的问题,这个问题怎么解决呢?

        咱们可以从开始考虑考虑这个问题,第一项是0的阶乘是1,第二项是1的阶乘也是1,第三项是2的阶乘1*2......

        由上面的图片就可以较清晰的看到,后项的阶乘其实是前项的阶乘乘i,知道这一点之后大概就知道怎么做了,咱们只需要使每一项的阶乘算完之后的值保留下来,到算下一项阶乘值直接用前一项的阶乘值,乘当前的i值就可以,这样可以避免很多次重复的计算,那么如何保留每次函数值算的阶乘值就是我们的目标,咱们都知道,函数里里面的变量生命周期就在函数内部,出了函数体就不存在了,如何使得里面的变量都存在就得使用关键字-static。通过static修饰局部变量可以改变局部变量的生命周期,使其拥有全局变量的性质,于是可以写出来程序3。

程序3

int fac(int i)
{
	static int tmp = 1;
	if (i == 0)
	{
		return 1;
	}
	else
	{
		tmp = tmp * i;
		return tmp;
	}
}

        如果按照我们的预期,函数应该就没用问题了,但是很不幸的是,输入较大点的数时依然出现了和之前程序一样的问题。

        程序依然运行时间过长,刚遇到这个问题时,我是很崩溃的,自我感觉分析的很完善了,但是不知道为什么还会出现这种错误,于是我开始进行程序的调试。

        

         当i=17时,大家有没有发现问题,阶乘的值已经超过的int的最大范围2^31-1了,这个问题在没有调试之前我是真的没想到会这样,int占8个字节,32个比特位,平时还真不容易溢出,这次程序的项数太多了,所以int类型的变量存不下,需要采用更大的变量存,于是改为程序4,重新运行程序,输入5,结果秒出。

程序4

#include <math.h>

double fac(int i)
{
	static double tmp = 1.0;
	if (i == 0)
	{
		return 1;
	}
	else
	{
		tmp = tmp * i;
		return tmp;
	}
}

总结

        在编写程序时,除了注意语法,算法之外,我们有时候也要多注意变量的类型,另外遇到程序BUG,一定要善于使用编译器的调试工具,DEBUG也是非常重要的一项能力。