算法效率与程序优化Word文档格式.docx
《算法效率与程序优化Word文档格式.docx》由会员分享,可在线阅读,更多相关《算法效率与程序优化Word文档格式.docx(52页珍藏版)》请在冰豆网上搜索。
(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,与除法运算速度几乎相当,都非常慢。
所以,取模运算也要尽量减少。
在大数运算而只要求求得MODN的值的题目中,应尽量利用数据类型的最大允许范围,在保证不超过MAXINT的前提下,尽量少执行MOD运算。
例如在循环数组、循环队列的应用中,取模运算必不可少,这时优化运算便十分重要。
可利用计数足够一定次数后再统一MOD,循环完后再MOD,使用中间变量记录MOD结果等方法减少次数。
在高精度计算中,许多人喜欢边运算边整理移位,从而在内层循环中有除、模运算各一次,效率极低。
应该利用int的数据范围,先将计算结果保存至当前位,在各位的运算结束后再统一整理。
以下是用统一整理法编写的高精度乘法函数,规模为10000位。
inta[10000]={0},b[10000]={0},c[10000]={0};
voidmul()
{inti,j,t;
10000;
i++)
10000-i;
c[i+j]+=a[i]*b[j];
9999;
{c[i+1]+=c[i]/10;
c[i]%=10;
}
以上函数运行后,平均用时276.55ms。
以下是边运算边整理的程序。
{c[i+j+1]+=(c[i+j]+a[i]*b[j])/10;
c[i+j]=(c[i+j]+a[i]*b[j])%10;
以上函数运行后,平均用时882.80ms。
统一整理与边整理边移位相比,快了3.2倍,有明显优势。
故尽量减少除法、取模运算的次数,是从常数级别降低时间复杂度的方法。
(7)大小比较
if(x>
y)x=y;
净运行时间39.1ms,与加法运算速度相当。
故比较运算也属于较快的基本运算。
二、位运算的速度
(1)左移、右移
x<
<
1;
x>
>
净运行时间无法测出,证明位运算速度极快。
而使用自乘计算需要64.1ms,自除运算需要164.85ms,所以尽可能使用位运算代替乘除。
(2)逻辑运算
t=x|y;
t=x^y;
t=x&
y;
净运行时间约30ms,比加法运算(约40ms)快较多,是因为全是按二进制位计算。
但加减与位运算关系并不大,所以利用位运算主要是利用左右移位的高速度。
三、数组运算的速度
(1)直接应用数组
for(i=0;
for(k=0;
k<
k++)
t=q[k];
净运行时间39.85ms。
这里计算了内层循环的时间。
若改为
t=q[0];
则净运行时间为1.50ms,很快,与202.3ms的循环时间相比,可以忽略。
故应用数组,速度很快,不必担心数组寻址耗时。
同时我们发现,循环耗时在各种运算中是很大的,仅次于乘除,故我们要尽量减少循环次数,能在一个循环中解决的问题不放在两个循环中,减少循环变量次数。
(2)二维数组
5000;
t=z[i][k];
实际运行时间为80.45ms,若规模扩至10000则10s内无法出解,由于频繁访问虚拟内存。
可以试想,若物理内存足够大,则运行时间约为320ms,仅为202.3ms的基准运行时间的3/2,差距似乎并不是很大;
由此推得其净运行时间约为120ms。
但相较加、减等简单操作,速度仍为3倍,尤其与几乎不需时间的一维数组相比差距巨大。
尤其是在计算中,二维数组元素经常调用,时间效率不可忽视。
所以,对于已知数目不多的同样大小的数组,可用几个变量名不同的一维数组表示,如x、y方向,两种不同参数,而不要滥用二维数组。
在滚动数组中,可用两个数组交替计算的方式,用二维数组同样较慢。
四、实数运算的速度
测试方法与“基本运算”类似。
(次数:
100000000,单位:
ms)
运算符
=
+
-
*
/
%
longint
43.05
31.3
-74.75
69.55
299.65
360.5
int64
41.45
14.95
7.9
566.4
1243.45
1858.85
double
46.15
10.25
12.6
33.65
1753.55
--
由上表可见,涉及乘除、取模时int64很慢,要慎用;
int显然最快,但对大数据要小心越界。
若一组变量中既有超出int的,又有不超过int的,则要分类处理,不要直接都定义成int64,尤其在乘除模较多的高精度过程中。
以上讨论了主要基本运算的速度问题。
概括起来说,除、模最慢,二维数组较慢,加减乘、逻辑位运算、比较大小较快,左右移位、一维数组、赋值几乎不需要时间。
而循环for或while语句十分特殊,它的运算速度大于判断大小、自加控制变量所用时间之和,无论采用内部if判断退出,还是在入口处判断,都回用去约200ms的时间。
所以尽量减少循环次数,是优化算法的关键。
对于双层或多层的循环,应把循环次数少的放在最外层,最大的循环位居最内部,以减少内层循环的执行次数。
第二章各种算法的速度
一、排序算法的速度
1.冒泡排序
20000;
a[i]=rand();
s=clock();
n;
i;
if(a[i]>
a[j])
{t=a[i];
a[i]=a[j];
a[j]=t;
运行时间:
1407ms
2.选择排序
s=clock();
{max=0;
if(a[j]>
a[max])
max=j;
b[i]=a[max];
a[max]=-1000000;
t=clock();
1220ms
3.插入排序
for(j=i-1;
j>
=0;
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)的排序,其中插入排序最快,且可以用指针加以优化。
从编程复杂度上,冒泡排序最简单。
从算法的稳定性上,插入排序是稳定的,即排序后关键字相同的记录顺序不改变,特别适用于表达式处理等问题。
一般的选择排序是不稳定的,但这里给出的程序由于使用了人类最原始的方法,即依次选择最大的并排除,故是稳定的。
冒泡排序是不稳定的,涉及必须保持数据原顺序的题目时不能选择冒泡排序,而必须选择稳定的排序方式。
以下试验所采用的环境是:
CPUIntelCore1.73GHz*2,内存512M,操作系统Windows7UltimateBeta,程序语言C。
编译环境Dev-c++,以下称为2号机。
由于CPU速度较慢,且操作系统占用资源较多,程序运行速度明显减慢,第一章的“基本运行测试”需要时间约为前者的2倍,即为406ms。
故第一章的程序运行时间此处应乘2。
4.快速排序的标准版
#defineMAX10000000
inta[MAX];
intp(intl,intr)
{intx=a[l],i=l-1,j=r+1,t;
while
(1)
{do{--j;
}while(a[j]<
x);
do{++i;
}while(a[i]>
if(i<
j)
a[i]=a[j];
a[j]=t;
elsereturnj;
voidq(intl,intr)
{intn;
if(l<
r)
{n=p(l,r);
q(l,n);
q(n+1,r);
2948ms。
注意:
不要以为三种平方级排序方法的速度与快速排序可比拟,因为平方级的数据范围是10000,而快速排序的范围是10000000。
对于10000的数据,快速排序只需3.1ms。
另外,快速排序不是稳定的排序,需要保持原顺序的不能用此法。
voidp(intl,intr)
r){
elsebreak;
p(l,j);
p(j+1,r);
若程序改为以上形式,则运行时间为2917ms,稍快了一些,是因为减少了函数调用次数。
对于函数调用,我们进行这样的测试。
test();
t=clock();
inttest()
n++;
净运行时间200ms
净运行时间84ms
净运行时间10ms
由此可见,调用函数本身并不浪费时间,仅相当于循环本身时间400ms的1/40,相当于加法80ms的1/8,是很快的运算。
但由于在函数内部需要进行现场保护,调用系统堆栈,所以用时大幅增加,定义变量后只是一个自增运算就用去120ms,相当于主程序中加法运算时间的3/2倍。
故函数中的运算比主程序中要慢,尤其是反复调用函数,会增加不必要的时间开销。
所以,一些简单的功能尽量在一个函数或主程序内完成,不要使用过多的函数;
涉及全局的变量不要在函数调用时由接口给出,再返回值,尽量使用全局变量。
这些方法可能使程序的可读性降低,不利于调试,但有利于提高时效,正如汇编语言程序比高级语言快一样。
5.优化的快速排序
(1)用插入排序优化
由于递归调用浪费大量时间,本算法的思想是,当首尾间距小于min时,改用效率较高的插入排序,减少反复递归。
这个想法是好的,但运行效果并不如人意。
当min=4时,程序运行时间降为2901ms,优化幅度不大,且增加了编程复杂度,故不宜采用。
其原因是递归调用、插入排序内部循环所用时间过长。
(2)用小数据判断优化
if(l+1<
while
(1){}//与普通快速排序相同
elseif(l+1==r&
&
a[l]<
a[r])
仅就区间长度为2的情况进行判断优化,减少一次递归调用,就能使运行时间缩短为2699ms,降低了200ms以上。
而区间长度不小于3的直接判断有困难。
快速排序运行时间(单位:
数据规模
10000
100000
1000000
10000000
原始快速排序
1.50
25.20
258.55
2716.45
优化快速排序
2.30
23.40
245.40
2564.80
6.归并排序
归并排序是一种稳定的排序方法,且时间效率与快速排序相同,都是O(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]<
a[from+1])
{t=a[from];
a[from]=a[from+1];
a[from+1]=t;
combine(from,mid);
combine(mid,to);
f=from;
r=mid;
i=from;
while(f!
=mid||r!
=to)
if(f!
=mid&
a[f]>
b[i++]=a[f++];
elseb[i++]=a[r++];
for(i=from;
to;
a[i]=b[i];
调用:
combine(0,MAX);
归并排序算法还可用于统计逆序对数。
所谓逆序对,即为在一个数组a中,满足i<
j且a[i]>
a[j](或a[i]<
a[j],依排序方向而定)的点(i,j)的个数。
voidsort(intfrom,intto)
tot++;
sort(from,mid);
sort(mid,to);
else
{b[i++]=a[r++];
=mid)
tot+=(mid-from);
主函数:
sort(0,n);
%d"
n*(n-1)/2-tot);
7.多关键字排序
多关键字排序,经常出现于要求按某要素排序姓名,该要素相同的按字典序排列的题目。
这样,此要素是第一关键字,姓名是第二关键字。
qist(0,n-1);
j=0;
for(i=1;
if(ist[lo[i]]!
=ist[lo[i-1]])
{qnameist(j,i-1);
j=i;
qnameist(j,n-1);
此算法的意思是,先按第一关键字快速排序,在从左至右扫描,找到每段相同的元素,再按第二关键字快速排序。
此算法复杂度仍为O(nlogn),且比两次快速排序要快,因为第二次排序已用O(n)的时间将其分为若干小块,排序效率大大提高。
8.字符串排序
voidqname(intl,intr)
{inti=l-1,j=r+1,t;
}while(strcmp(name[lo[j]],name[lo[i]])>
0);
}while(strcmp(name[lo[j]],name[lo[i]])<
{t=lo[i];
lo[i]=lo[j];
lo[j]=t;
qname(l,j);
qname(j+1,r);
上述排序方法,实质上是将普通快速排序中的数大小比较换成字符串大小比较。
为了避免字符串交换浪费时间,采用了类似指针的定位数组,只需交换定位数组的元素即可。
当然,用指针直接实现效率更高。
关于字符串比较函数strcmp的时间效率,我们有如下试验:
100;
{a[i]=rand();
b[i]=rand();
if(strcmp(a,b)==0)
break;
1046ms,其中净运行时间约640ms。
这是对长度100的字符串进行10^8次比较的时间。
故strcmp的效率即为约8次整数比较所需时间,比自己编写的函数快。
intstr(chara[],charb[])
{inti=0;
while(a[i]!
=0||b[i]!
=0)
{if(a[i]>
b[i])
return1;
if(a[i]<
return-1;
return0;
1131ms,净运行时间约725ms,相当于9次整数比较时间,比标准库函数稍慢。
显然,标准库函数是经过精心优化的,我们在有库函数可用时尽量用库函数,不仅降低编程复杂度,降低错误率,还能提高时间效率。
9.Hash排序
#defineMOD999997//Asabigprime
#defineMAX10000//Asthemaxlengthofthestring
intELFhash(char*key)
{unsignedlongh=0;
while(*key)
{h=(h<
4)+*key++;
unsignedlongg=h&
0Xf0000000L;
if(g)h^=g>
24;
h&
=~g;
returnh%MOD;
Hash排序,就是先对读入的字符串求Hash值。
第一种方案是,再对Hash值进行快速排序,最后应用二分搜索查找。
此时无需MOD大质数。
第二种方案是,将Hash值MOD后,放入Hash表中,并用开散列等方法处理冲突。
显然,第二种方案更优,但编程复杂度也较高。
综上所述,在选择适当的排序算法时,要注意时间复杂度和编程复杂度,同时保证满足题意。
二、最短路算法的比较
在图论中,最短路算法是常见的。
对于常见的几种算法,到底哪种更优,还是各有千秋?
我们不妨一试。
以下实验在1号机上进行,均为试验20次随机数取平均值的结果。
1.单源最短路径——dijkstra算法
voiddijkstra()
{inti,j,min=0;
dis[i]=d[0][i];
for(i=1;
{min=n;
if(!
use[j]&
d[i][j]<
d[i][min])
min=j;
use[min]=1;
if(dis[min]+d[min][j]<
dis[j])
dis[j]=dis[min]+d[min][j];
随机数产生器:
MAX;
if(i!
=j)
d[i][j]=rand();
当MAX=2000时(即点数为2000个),运行时间:
28.2ms。
当MAX更大时,内存会使用过多,故在1s时限内至多运行规模为2000的单源最短路径35次。
2.单源最短路径——Ford算法
intford()
d[i]=MAXINT;