算法效率与程序优化

2019-07-13 16:18

算法效率与程序优化

在信息学竞赛中,常遇到程序运行超时的情况。然而,同一个程序设计思想,用不同算法,会有不同的运行效率;而即使是同样的算法,由于在代码的细节方面设计有所不同,执行起来效率也会有所不同。当遇到所需时间较长的问题时,一个常数级优化可能是AC的关键所在。下面,我们就从代码细节与算法设计两方面,比较不同程序执行时间的异同,从而选择其中较优的算法,提高程序运行效率。

本试验所采用的环境是: CPU Celeron 3.06GHz,内存248M,操作系统Windows XP SP2,程序语言C。编译环境Dev-c++。以下称为1号机。配置略好于NOIP标准测试机CPU 2.0GHz。

第一章 各种运算的速度

一、基本运算的速度

为了增强算法效率的计算准确性,我们采用重复试验20次取平均值的做法。每次试验运行100000000次。

基本运行时间,是指在准备计算的运算复杂度之外,只包括循环控制变量的加减与比较所消耗的时间。要从实际运行时间中减去基本运行时间,才是这种运算真正的运行时间,称为净运行时间。

#include main() { int i,j;

double a,b,sum=0; for(j=0;j<20;j++)

{ //此处添加随机数产生 a=clock();

for(i=0;i<100000000;i++);//此处添加运算 b=clock();

printf(\ sum+=b-a; }

printf(\ getch(); }

运行平均时间是:202.3ms。 (1)赋值运算

净运行时间0.8ms,与基本运行时间202.3ms相比,可忽略不计,故以后将赋值运算作为基本运行时间的一部分,不予考虑。 (2)加法运算 产生随机数: x=rand();

y=rand();

循环内加法: t=x+y;

下面的各种简单运算只是更改运算符即可,不再写出代码。 净运行时间41.45ms,即:在1s的时限中,共可进行 (1000-202.3)/ 41.45*10^8=1.9*10^9次加法运算,即:只通过循环、加法和赋值的运算次数不超过1.9*10^9.。

而应用+=运算,与普通加法时间几乎相同,所以+=只是一种方便书写的方法,没有实质效果。同样的,各种自运算并不能提高效率。 (3)减法运算

净运行时间42.95ms,与加法运算基本相同。可见,在计算机内部实现中,把减法变成加法的求补码过程是较快的,而按位相加的过程占据了较多的时间,借用化学中的一句术语,可以称为整个运算的“速控步”。当然,这个“速控步”的运行速度受计算机本身制约,我们无法优化。在下面的算法设计中,还会遇到整个算法的“速控步”,针对这类情况,我们要对占用时间最多的步骤进行细心优化,减少常数级运算次数。

(4)乘法运算

净运行时间58.25ms,明显比加减法要慢,但不像某些人想象的那样慢,至少速度大于加减法的1/2。所以在实际编程时,没有必要把两个或三个加法变成乘法,其实不如元素乘常数来得快。不必“谈乘色变”,实际乘法作为CPU的一种基本运算,速度还是很快的。

以上四种运算速度都很快,比循环所需时间少很多,在普通的算法中,每个最内层循环约含有4-5个加、减、乘运算,故整个算法的运行时间约为循环本身所需时间的2倍。

(5)除法运算

净运行时间1210.2ms,是四种常规运算中最慢的一种,耗时是加法的28倍,是乘法的21.5倍,确实如常人所说“慢几十倍”,一秒的时限内只能运行8.26*10^7次,不足一亿次,远大于循环时间。所以,在计算时应尽量避免除法,尤其是在对时间要求较高的内层循环,尽量不安排除法,如果整个循环中值不变,可以使用在循环外预处理并用一个变量记录,循环内再调用该变量的方法,可以大大提高程序运行效率。

(6)取模运算

净运行时间1178.15ms,与除法运算速度几乎相当,都非常慢。所以,取模运算也要尽量减少。在大数运算而只要求求得MOD N的值的题目中,应尽量利用数据类型的最大允许范围,在保证不超过MAXINT的前提下,尽量少执行MOD运算。例如在循环数组、循环队列的应用中,取模运算必不可少,这时优化运算便十分重要。可利用计数足够一定次数后再统一MOD,循环完后再MOD,使用中间变量记录MOD结果等方法减少次数。

在高精度计算中,许多人喜欢边运算边整理移位,从而在内层循环中有除、模运算各一次,效率极低。应该利用int的数据范围,先将计算结果保存至当前位,在各位的运算结束后再统一整理。

以下是用统一整理法编写的高精度乘法函数,规模为10000位。 int a[10000]={0},b[10000]={0},c[10000]={0}; void mul() { int i,j,t;

for(i=0;i<10000;i++) for(j=0;j<10000-i;j++) c[i+j]+=a[i]*b[j]; for(i=0;i<9999;i++) { c[i+1]+=c[i]/10; c[i]%=10;

} }

以上函数运行后,平均用时276.55ms。 以下是边运算边整理的程序。 void mul() { int i,j,t;

for(i=0;i<10000;i++) for(j=0;j<10000-i;j++)

{ c[i+j+1]+=(c[i+j]+a[i]*b[j])/10; c[i+j]=(c[i+j]+a[i]*b[j]); } }

以上函数运行后,平均用时882.80ms。统一整理与边整理边移位相比,快了3.2倍,有明显优势。故尽量减少除法、取模运算的次数,是从常数级别降低时间复杂度的方法。

(7)大小比较 if(x>y) x=y;

净运行时间39.1ms,与加法运算速度相当。故比较运算也属于较快的基本运算。 二、位运算的速度 (1)左移、右移 x<<1; x>>1;

净运行时间无法测出,证明位运算速度极快。而使用自乘计算需要64.1ms,自除运算需要164.85ms,所以尽可能使用位运算代替乘除。

(2)逻辑运算 t=x|y; t=x^y; t=x&y;

净运行时间约30ms,比加法运算(约40ms)快较多,是因为全是按二进制位计算。但加减与位运算关系并不大,所以利用位运算主要是利用左右移位的高速度。

三、数组运算的速度 (1)直接应用数组 for(i=0;i<10000;i++)

for(k=0;k<10000;k++) t=q[k];

净运行时间39.85ms。这里计算了内层循环的时间。若改为 for(i=0;i<100000000;i++) t=q[0];

则净运行时间为1.50ms,很快,与202.3ms的循环时间相比,可以忽略。故应用数组,速度很快,不必担心数组寻址耗时。同时我们发现,循环耗时在各种运算中是很大的,仅次于乘除,故我们要尽量减少循环次数,能在一个循环中解决的问题不放在两个循环中,减少循环变量次数。

(2)二维数组 for(i=0;i<5000;i++) for(k=0;k<5000;k++) t=z[i][k];

实际运行时间为80.45ms,若规模扩至10000则10s内无法出解,由于频繁访问虚拟内存。可以试想,若物理内存足够大,则运行时间约为320ms,仅为202.3ms的基准运行时间

的3/2,差距似乎并不是很大;由此推得其净运行时间约为120ms。但相较加、减等简单操作,速度仍为3倍,尤其与几乎不需时间的一维数组相比差距巨大。尤其是在计算中,二维数组元素经常调用,时间效率不可忽视。所以,对于已知数目不多的同样大小的数组,可用几个变量名不同的一维数组表示,如x、y方向,两种不同参数,而不要滥用二维数组。在滚动数组中,可用两个数组交替计算的方式,用二维数组同样较慢。

四、实数运算的速度

测试方法与“基本运算”类似。(次数:100000000,单位:ms) 运算符 long int int64 double = 43.05 41.45 46.15 + 31.3 14.95 10.25 - -74.75 7.9 12.6 * 69.55 566.4 33.65 / 299.65 1243.45 1753.55 % 360.5 1858.85 -- 由上表可见,涉及乘除、取模时int64很慢,要慎用;int显然最快,但对大数据要小心越界。若一组变量中既有超出int的,又有不超过int的,则要分类处理,不要直接都定义成int64,尤其在乘除模较多的高精度过程中。

以上讨论了主要基本运算的速度问题。概括起来说,除、模最慢,二维数组较慢,加减乘、逻辑位运算、比较大小较快,左右移位、一维数组、赋值几乎不需要时间。而循环for或while语句十分特殊,它的运算速度大于判断大小、自加控制变量所用时间之和,无论采用内部if判断退出,还是在入口处判断,都回用去约200ms的时间。所以尽量减少循环次数,是优化算法的关键。对于双层或多层的循环,应把循环次数少的放在最外层,最大的循环位居最内部,以减少内层循环的执行次数。

第二章 各种算法的速度

一、排序算法的速度 1.冒泡排序

for(i=0;i<20000;i++) a[i]=rand(); s=clock();

for(i=0;ia[j]) { t=a[i]; a[i]=a[j]; a[j]=t; }

b=clock(); 运行时间:1407ms 2.选择排序

for(i=0;i<20000;i++) a[i]=rand(); s=clock();

for(i=0;i

for(j=0;ja[max])

max=j; b[i]=a[max];

a[max]=-1000000; }

t=clock();

运行时间:1220ms 3.插入排序

for(i=0;i<20000;i++) a[i]=rand(); s=clock(); for(i=0;i

for(j=i-1;j>=0;j--) if(a[j]>t) break;

for(l=i;l>j+1;l--) a[l]=a[l-1]; a[j+1]=t; }

t=clock(); 运行时间:984ms

以上三种都是O(n^2)的排序,其中插入排序最快,且可以用指针加以优化。从编程复杂度上,冒泡排序最简单。从算法的稳定性上,插入排序是稳定的,即排序后关键字相同的记录顺序不改变,特别适用于表达式处理等问题。一般的选择排序是不稳定的,但这里给出的程序由于使用了人类最原始的方法,即依次选择最大的并排除,故是稳定的。冒泡排序是不稳定的,涉及必须保持数据原顺序的题目时不能选择冒泡排序,而必须选择稳定的排序方式。

以下试验所采用的环境是: CPU Intel Core 1.73GHz*2,内存512M,操作系统Windows 7 Ultimate Beta,程序语言C。编译环境Dev-c++,以下称为2号机。由于CPU速度较慢,且操作系统占用资源较多,程序运行速度明显减慢,第一章的“基本运行测试”需要时间约为前者的2倍,即为406ms。故第一章的程序运行时间此处应乘2。

4.快速排序的标准版 #define MAX 10000000 int a[MAX]; int p(int l,int r)

{ int x=a[l],i=l-1,j=r+1,t; while(1) { do{--j;

}while(a[j]

}while(a[i]>x); if(i

{ t=a[i];a[i]=a[j];a[j]=t; }


算法效率与程序优化.doc 将本文的Word文档下载到电脑 下载失败或者文档不完整,请联系客服人员解决!

下一篇:2019七一建党节活动总结,提高党员的责任感和使命感

相关阅读
本类排行
× 注册会员免费下载(下载后可以自由复制和排版)

马上注册会员

注:下载文档有可能“只有目录或者内容不全”等情况,请下载之前注意辨别,如果您已付费且无法下载或内容有问题,请联系我们协助你处理。
微信: QQ: