算法效率与程序优化.docx
- 文档编号:6767500
- 上传时间:2023-01-10
- 格式:DOCX
- 页数:43
- 大小:86.19KB
算法效率与程序优化.docx
《算法效率与程序优化.docx》由会员分享,可在线阅读,更多相关《算法效率与程序优化.docx(43页珍藏版)》请在冰豆网上搜索。
算法效率与程序优化
算法效率与程序优化
在信息学竞赛中,常遇到程序运行超时的情况。
然而,同一个程序设计思想,用不同算法,会有不同的运行效率;而即使是同样的算法,由于在代码的细节方面设计有所不同,执行起来效率也会有所不同。
当遇到所需时间较长的问题时,一个常数级优化可能是AC的关键所在。
下面,我们就从代码
细节与算法设计两方面,比较不同程序执行时间的异同,从而选择其中较优的算法,提高程序运行效率。
本试验所采用的环境是:
CPUCeleron,内存248M,操作系统WindowsXPSP2,程序语言(
C。
编
译环境Dev-c++。
以下称为
1号机。
配置略好于
NOIP标准测试机CPU。
第一章
各种运算的速度
一、基本运算的速度
为了增强算法效率的计算准确性,我们采用重复试验20次取平均值的做法。
每次试验运行0次
基本运行时间,是指在准备计算的运算复杂度之外,只包括循环控制变量的加减与比较所消耗的
时间。
要从实际运行时间中减去基本运行时间,才是这种运算真正的运行时间,称为净运行时间。
而应用+=运算,与普通加法时间几乎相同,所以+=只是一种方便书写的方法,没有实质效果。
同
样的,各种自运算并不能提高效率。
(3)减法运算
净运行时间,与加法运算基本相同。
可见,在计算机内部实现中,把减法变成加法的求补码过程是较快的,而按位相加的过程占据了较多的时间,借用化学中的一句术语,可以称为整个运算的“速控步”。
当然,这个“速控步”的运行速度受计算机本身制约,我们无法优化。
在下面的算法设计中,还会遇到整个算法的“速控步”,针对这类情况,我们要对占用时间最多的步骤进行细心优化,减少常数级运算次数。
(4)乘法运算
净运行时间,明显比加减法要慢,但不像某些人想象的那样慢,至少速度大于加减法的1/2。
所
以在实际编程时,没有必要把两个或三个加法变成乘法,其实不如元素乘常数来得快。
不必“谈乘色变”,实际乘法作为CPU的一种基本运算,速度还是很快的。
以上四种运算速度都很快,比循环所需时间少很多,在普通的算法中,每个最内层循环约含有4-5个加、减、乘运算,故整个算法的运行时间约为循环本身所需时间的2倍。
(5)除法运算
净运行时间,是四种常规运算中最慢的一种,耗时是加法的28倍,是乘法的倍,确实如常人所说
“慢几十倍”,一秒的时限内只能运行*10A7次,不足一亿次,远大于循环时间。
所以,在计算时应尽
量避免除法,尤其是在对时间要求较高的内层循环,尽量不安排除法,如果整个循环中值不变,可以使用在循环外预处理并用一个变量记录,循环内再调用该变量的方法,可以大大提高程序运行效率。
(6)取模运算
净运行时间,与除法运算速度几乎相当,都非常慢。
所以,取模运算也要尽量减少。
在大数运算
而只要求求得MODN的值的题目中,应尽量利用数据类型的最大允许范围,在保证不超过MAXINT的前
提下,尽量少执行MOD!
算。
例如在循环数组、循环队列的应用中,取模运算必不可少,这时优化运算
便十分重要。
可利用计数足够一定次数后再统一MOD循环完后再MOD使用中间变量记录MOD结果等
方法减少次数。
在高精度计算中,许多人喜欢边运算边整理移位,从而在内层循环中有除、模运算各一次,效率
极低。
应该利用int的数据范围,先将计算结果保存至当前位,在各位的运算结束后再统一整理。
以下是用统一整理法编写的高精度乘法函数,规模为10000位。
inta[10000]={0},b[10000]={0},c[10000]={0};
voidmul()
{inti,j,t;
for(i=0;iv10000;i++)
for(j=0;jv10000-i;j++)
c[i+j]+=a[i]*b[j];
for(i=0;i<9999;i++)
{c[i+1]+=c[i]/10;
c[i]%=10;
}
}
以上函数运行后,平均用时。
以下是边运算边整理的程序。
voidmul()
{inti,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])%10;
}
}
以上函数运行后,平均用时。
统一整理与边整理边移位相比,快了倍,有明显优势。
故尽量减少
除法、取模运算的次数,是从常数级别降低时间复杂度的方法。
(7)大小比较
if(x>y)x=y;
净运行时间,与加法运算速度相当。
故比较运算也属于较快的基本运算。
二、位运算的速度
(1)左移、右移
x<<1;x>>1;
净运行时间无法测岀,证明位运算速度极快。
而使用自乘计算需要,自除运算需要,所以尽可能
使用位运算代替乘除。
(2)逻辑运算
t=x|y;t=xAy;t=x&y;
净运行时间约30ms,比加法运算(约40ms)快较多,是因为全是按二进制位计算。
但加减与位运
算关系并不大,所以利用位运算主要是利用左右移位的高速度。
三、数组运算的速度
(1)直接应用数组
for(i=0;i<10000;i++)
for(k=0;k<10000;k++)
t=q[k];
净运行时间。
这里计算了内层循环的时间。
若改为
for(i=0;i<0;i++)
t=q[0];
则净运行时间为,很快,与的循环时间相比,可以忽略。
故应用数组,速度很快,不必担心数组寻址耗时。
同时我们发现,循环耗时在各种运算中是很大的,仅次于乘除,故我们要尽量减少循环次数,能在一个循环中解决的问题不放在两个循环中,减少循环变量次数。
(2)二维数组
for(i=0;i<5000;i++)
for(k=0;k<5000;k++)
t=z[i][k];
实际运行时间为,若规模扩至10000则10s内无法岀解,由于频繁访问虚拟内存。
可以试想,若物
理内存足够大,则运行时间约为320ms,仅为的基准运行时间的3/2,差距似乎并不是很大;由此推得
其净运行时间约为120ms。
但相较加、减等简单操作,速度仍为3倍,尤其与几乎不需时间的一维数组
相比差距巨大。
尤其是在计算中,二维数组元素经常调用,时间效率不可忽视。
所以,对于已知数目不多的同样大小的数组,可用几个变量名不同的一维数组表示,如x、y方向,两种不同参数,而不要
滥用二维数组。
在滚动数组中,可用两个数组交替计算的方式,用二维数组同样较慢。
四、实数运算的速度
测试方法与“基本运算”类似。
运算符
=
+
-
*
/
%
longint
int64
double
--
由上表可见,涉及乘除、取模时int64很慢,要慎用;int显然最快,但对大数据要小心越界。
若
一组变量中既有超岀int的,又有不超过int的,则要分类处理,不要直接都定义成int64,尤其在乘
除模较多的高精度过程中。
以上讨论了主要基本运算的速度问题。
概括起来说,除、模最慢,二维数组较慢,加减乘、逻辑位运算、比较大小较快,左右移位、一维数组、赋值几乎不需要时间。
而循环for或while语句十分特
殊,它的运算速度大于判断大小、自加控制变量所用时间之和,无论采用内部if判断退岀,还是在入
口处判断,都回用去约200ms的时间。
所以尽量减少循环次数,是优化算法的关键。
对于双层或多层的
循环,应把循环次数少的放在最外层,最大的循环位居最内部,以减少内层循环的执行次数。
第二章各种算法的速度
一、排序算法的速度
1.冒泡排序
for(i=0;i<20000;i++)
a[i]=rand();
s=clock();
for(i=0;i for(j=0;j if(a[i]>a[j]) b=clock(); 运行时间: 1407ms 2.选择排序 for(i=0;i<20000;i++) a[i]=rand(); s=clock(); for(i=0;i {max=0; for(j=0;j if(a[j]>a[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 {t=a[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 以上三种都是0(n^2)的排序,其中插入排序最快,且可以用指针加以优化。 从编程复杂度上,冒泡排序最简单。 从算法的稳定性上,插入排序是稳定的,即排序后关键字相同的记录顺序不改变,特别适用于表达式处理等问题。 一般的选择排序是不稳定的,但这里给出的程序由于使用了人类最原始的方法,即依次选择最大的并排除,故是稳定的。 冒泡排序是不稳定的,涉及必须保持数据原顺序的题目时不能选择冒泡排序,而必须选择稳定的排序方式。 以下试验所采用的环境是: CPUIntelCore*2,内存512M,操作系统Windows7UltimateBeta, 程序语言C。 编译环境Dev-c++,以下称为2号机。 由于CPU速度较慢,且操作系统占用资源较多,程序运行速度明显减慢,第一章的“基本运行测试”需要时间约为前者的2倍,即为406ms。 故第一章的 程序运行时间此处应乘2。 4.快速排序的标准版 #defineMAX inta[MAX]; intp(intl,intr) {intx=a[l],i=l-1,j=r+1,t; while⑴ {do{--j; }while(a[j] do{++i; }while(a[i]>x); if(i {t=a[i];a[i]=a[j];a[j]=t; } elsereturnj; } } voidq(intl,intr) {intn; if(l {n=p(l,r); q(l,n); q(n+1,r); } } 运行时间: 2948ms。 注意: 不要以为三种平方级排序方法的速度与快速排序可比拟,因为平方级的数据范围是10000,而快速排序的范围是。 对于10000的数据,快速排序只需。 另外,快速排序不是 稳定的排序,需要保持原顺序的不能用此法。 voidp(intl,intr) {intx=a[l],i=l-1,j=r+1,t; if(l while (1) {do{--j; }while(a[j] do{++i; }while(a[i]>x); if(i {t=a[i];a[i]=a[j];a[j]=t; } elsebreak; } P(l,j); P(j+1,r); } } 若程序改为以上形式,则运行时间为2917ms,稍快了一些,是因为减少了函数调用次数。 对于函 数调用,我们进行这样的测试。 由此可见,调用函数本身并不浪费时间,仅相当于循环本身时间400ms的1/40,相当于加法80ms 的1/8,是很快的运算。 但由于在函数内部需要进行现场保护,调用系统堆栈,所以用时大幅增加,定义变量后只是一个自增运算就用去120ms,相当于主程序中加法运算时间的3/2倍。 故函数中的运算比 主程序中要慢,尤其是反复调用函数,会增加不必要的时间开销。 所以,一些简单的功能尽量在一个函数或主程序内完成,不要使用过多的函数;涉及全局的变量不要在函数调用时由接口给岀,再返回值,尽量使用全局变量。 这些方法可能使程序的可读性降低,不利于调试,但有利于提高时效,正如汇编语言程序比高级语言快一样。 5.优化的快速排序 (1)用插入排序优化 由于递归调用浪费大量时间,本算法的思想是,当首尾间距小于min时,改用效率较高的插入排 序,减少反复递归。 这个想法是好的,但运行效果并不如人意。 当min=4时,程序运行时间降为 2901ms,优化幅度不大,且增加了编程复杂度,故不宜采用。 其原因是递归调用、插入排序内部循环所用时间过长。 (2)用小数据判断优化 if(l+1 while (1){} 并排序 归并排序是一种稳定的排序方法,且时间效率与快速排序相同,都是0(nlogn)。 但归并排序比快 速排序的常数因子大,故快速排序还是最快的排序方法。 归并排序则适用于有特殊要求的题目,如不满足交换律的表达式处理。 inta[MAX],b[MAX]; voidcombine(intfrom,intto) {inti,t,mid=(from+to)/2,f,r; if(from==to||from+1==to) return; if(from+2==to) {if(a[from] {t=a[from]; a[from]=a[from+1]; a[from+1]=t; } return; } combine(from,mid); combine(mid,to); f=from; r=mid; i=from; while(f! =mid||r! =to)| if(f! =mid&&a[f]>a[r]) b[i++]=a[f++]; elseb[i++]=a[r++]; for(i=from;i a[i]=b[i]; } 调用: combine(0,MAX); 归并排序算法还可用于统计逆序对数。 所谓逆序对,即为在一个数组a中,满足i (或a[i] voidsort(intfrom,intto) {inti,t,mid=(from+to)/2,f,r; if(from==to||from+1==to) return; if(from+2==to)
- 配套讲稿:
如PPT文件的首页显示word图标,表示该PPT已包含配套word讲稿。双击word图标可打开word文档。
- 特殊限制:
部分文档作品中含有的国旗、国徽等图片,仅作为作品整体效果示例展示,禁止商用。设计者仅对作品中独创性部分享有著作权。
- 关 键 词:
- 算法 效率 程序 优化