C语言常规优化策略
2 条件语句优化
2.1 多分枝条件语句优化
多分枝条件语句一般采用switch语句,这样的程序无论从清晰性和效率上都比原来的程序要好。例如下面的函数采用三种函数形式分别计算x在Alpha,Beta和Gamma处的值,通常的写法为:
int f(int x)
{
int y;
if (x==Alpha)
y=f1(x);
else
if (x==Beta)
y=f2(x);
else
if (x==Gamma)
y=f3(x);
return y;
}
采用switch语句可写成:
int f (int x)
{
int y;
switch (x)
{
case Alpha:
y=f1(x);
break;
case Beta:
y=f2(x);
break;
case Gamma:
y=f3(x);
break;
}
return y;
}
值得指出的是,在多分支条件语句不能改造成语句时,我们一般采用紧缩的写法,例如我们要计算下面的分段函数值
f(x)= f1(x) x<=Alpha
f2(x) Alpha <x<=Beta
f3(x) Beta<x<=Gamma
f4(x) Gamma<x
紧缩的写法为
int f(int x)
{
int y;
if (x<=Alpha)
y=f1(x);
else if (x<=Beta)
y=f2(x);
else if (x<=Gamma)
y=f3(x);
else
y=f4(x);
return y;
}
2.2 复杂条件分析与条件表达式化简
有时通过对复杂的条件表达式进行分析,将复杂条件表达式简化, 可以提高代码的效率,我们通过几个例子来说明复杂条件分析化简的方法。
2.2.1 三角形测试问题
给定三个正整数a、b、c,问它们是否构成一个三角形的三条边?
三个整数a、b、c构成三角形三边的充要条件为:
a+b>c (1)
b+c>a (2)
c+a>b (3)
一般来说,只要有了上述三个条件,我们不必再强调a、b、c>0,这些条件已经蕴含在上述三个条件中,如由(1)、(3)左、右两边分别相加化简后就可以得到a>0。由此得到三角形测试问题求解的第一个程序:
typedef int BOOL;
#define TRUE 1
#define FALSE 0
Bool IsTriangle(int a, int b, int c)
{
if (a+b>c && b+c>a && c+a>b)
return TRUE;
return FALSE;
}
上述程序没有考虑计算溢出问题,因为a+b,b+c和c+a均可能超过一个整数的范围。
为避免计算溢出,可采用长整数算术运算和比较运算,但必须考虑到机器中的长整数必须比整数的表示范围要大才行。例如在Windows 95中采用Visual C++编程时,整数和长整数均为32位,如果是这样,这种改造就没意义了。相应代码为:
Bool IsTriangle(int a, int b, int c)
{
if ((long)a+(long)b>(long)c &&
(long)b+(long)c>(long)a &&
(long)c+(long)a>(long)b)
return TRUE;
return FALSE;
}
这一代码的使用是绝对安全的,唯一的问题是采用长整数算术与比较将会降低效率。如果我们要绕过长整数算术,同时又要保证代码的安全,可以对判断条件进行变换,例如,虽然三个加法会导致溢出,但只要a,b,c>0,c-b,b-a,a-c却不会产生溢出,因此可将条件(1)、(2)、(3)改变为相应的代码为:
Bool IsTriangle(int a, int b, int c)
{
if (a>0 && b>0 && c>0 &&
a>c-b && b>a-c && c>b-a)
return TRUE;
return FALSE;
}
代码中显示地加入了a,b,c>0测试,而在逻辑上由条件(4)、(5)、(6)是能蕴含这一结论的,这是否冗余的操作呢?只要注意到计算机内的算术操作受限于表示精度,同一般意义下的算术操作存在根本的差别,我们就知道这三个条件不能省略,否则可能导致减法的溢出,如a=b=-32000,整数为16位字长表示时就能明白。但在这一具体例子中,能否构造出满足条件(4)、(5)、(6)的整数a、b、c,而它们不全是正整数,有兴趣的读者可以一试。
至此,我们给出了三角形测试的三个版本,应该说三个版本都不能令人完全满意。为了照顾到代码的安全性和效率,判断条件变得越来越复杂了。下面我们就给出一个更复杂的版本,可是在这两个方面都能令人愉快,从这一例子中得到的经验是:有时一个简单的问题是需要进行复杂处理的。
不失一般性,设a>=b>=c,则a、b、c构成一个三角形三边的主要条件为
c>0且b+c>a
如果三个数是按上述要求有序排列的,则可以直接进行比较(当然为避免加法溢出,b+c>a 应换为b>a-c或c>a-b),否则需将这三个数进行排序,在下面的程序中,三个元素的排序用一个宏SORT3实现,而需要使用到两个整数交换的过程则由宏SWAP完成,这里我们利用了一个临时变量作为缓冲。”/”为宏的续行标志.
#define SWAP(a,b) /
{ /
int nTmp; /
/
nTmp=a; /
a=b; /
b=nTmp; /
}
#define SORT3(a, b, c) /
{ /
if (a<b) SWAP(a,b); /
if (b<c) SWAP(b,c); /
if (a<b) SWAP(a,b); /
}
BOOL IsTriangle(int a, int b, int c)
{
SORT3(a,b,c);
if (c>0 && c>a-b)
return TRUE;
return FALSE;
}
2.2.2 数组元素查找与标志技术
假设要从一个整型数组中查找某一值,若找到相应的元素,则返回该值所在数组元素的下标。这一问题有如下直观的线性搜索代码:
BOOL FindElem(int *pnIndex, int nValue, int *a, int nSize)
{
BOOL bFind; // 是否进一步查找的标志
int i;
bFind=TRUE;
i=0;
while (i <nSize && bFind)
{
if (a[i]==nValue)
{
*pnIndex=i;
bFind=FALSE;
}
i++;
}
return !bFind;
}
代码中,nValue为查找值;a为整型数组,nSize为数组大小,pnIndex为整型变量指针,用于找到值后存放下标。当搜索成功时,函数返回真,否则返回假。值得注意的是:循环中的两个条件和不可变换,否则会导致数组上溢。
有一种经典的技术用于提高上述代码的效率,这就是所谓的标记技术。其思想是在数组的后面插入要查找的元素值,这样就能够保证搜索过程总是能够成功,从而可以将循环条件表达式简化。查找成功与否则在跳出循环后由i的值进行判断,当i在标记之前时,说明该值在数组中,否则不在。
BOOL FindElem(int *pnIndex, int nValue, int *a, int nSize)
{
BOOL bFind; // 是否进一步查找的标志
int i;
bFind=TRUE;
i=-1;
a[nSize]=nValue; // 设置标志
while (bFind)
{
i++;
if (a[i]==nValue)
bFind=FALSE;
}
if (i<nSize)
{
*pnIndex=i;
return TRUE;
}
return FALSE;
}
代码要求数组大小比实际存放数据的长度多出一个单位,这必须在数组分配时予留,否则上述代码是错误的,因为a[nSize]并不属于程序员自己管理的空间。任何程序员都无权使用不属于自己的空间,哪怕只是一点点也不行,如果程序员不按章办事,可能导致的后果将是非常严重的。
如果程序员喜欢使用紧凑的空间,不想在分配数组时为标记予留一个单元,我们也可以给大家提供一种变通的方法:可以将数组的最后一个单元腾出来作为标记使用,不过循环结束一定要还原,并记住数组的最后一个元素还没有检查过。为什么不在把数组最后一个元素腾出来时就进行检查呢?因为按照我们前面所给出的代码逻辑,如果数组中有多个要查找的元素,我们总是返回其中下标最小者,我们不想破坏这一约定。
BOOL FindElem(int *pnIndex, int nValue, int *a, int nSize)
{
BOOL bFind; // 是否进一步查找的标志
int i, nTmp;
bFind=TRUE;
i=-1;
nTmp=a[nSize-1]; // 用于存级最后一个数组元素
a[nSize-1]=nValue; // 设置标志
while (bFind)
{
i++;
if (a[i]==nValue)
bFind=FALSE;
}
a[nSize-1]=nTmp; // 恢复最后一个元素
if (i<nSize-1 || (i==nSize-1 && a[nSize-1]==nValue))
{
*pnIndex=i;
return TRUE;
}
return FALSE;
}
2.2.3 线段相交测试
一个线段可用起始点和终点进行指述:
typedef struct tagSegment
{
Point pt0;
Point pt1;
} Segment;
对于给定的两条线段S1,S2,试确定它们是否相交。
线段与直线不同,其两端是固定的,无法延长,因此线段相交的测试与直线相交测试不同。当然可以利用直线相交的方法来判断线段是否相交,其方法如下:
(1) 将两根线段视为直线并求其交点;
(2) 判断该交点是否在线段S1和S2内。
这一方法存在两个不足:一是求直线方程及交点不可避免地涉及浮点运算;其二是大量的参数需要确定,其计算量比较大。鉴于此,我们改换问题求解的思路。在直线的扫描转换算法中,我们已经讲到一条直线将平面划分成三个部分。利用这一结论及观察我们可以得到:三个点P0,P1,P2之间的位置关系为
(1) det(P0,P1,P2)>0, P0,P1,P2逆时针排列;
(2) det(P0,P1,P2)>0, P0,P1,P2共线;
(3) det(P0,P1,P2)<0, P0,P1,P2顺时针排列。
其中
|x0 y0 1|
det(P0,P1,P2)=|x1 y1 1|
|x2 y2 1|
设给定的两条线段为P0P1和Q0Q1,观察可知,P0P1与Q0Q1相交,则下述条件之一成立:
(1) P0, P1在Q0Q1的两边,Q0, Q1在P0P1的两边,即det(P0,P1,Q0)与det(P0,P1,Q1)异号且det(Q0,Q1,P0)与det(Q0,Q1,P1)异号。
(2) 如果det(P0,P1,Q0)=0或det(P0,P1,Q1)=0,只要Q0或Q1在线段P0P1内,则两线段相交。
(3) 如果det(Q0,Q1,P0)=0或det(Q0,Q1,P1)=0,只要P0或P1在线段Q0Q1内,则两线段相交。
记
s0=sgn(det(P0,P1,Q0))
s1=sgn(det(P0,P1,Q1))
t0= sgn(det(Q0,Q1,P0))
t1= sgn(det(Q0,Q1,P1))
从而相交的条件为
(s0s1<0 && t0t1<0) ||
(s0==0 && PtInSegment(Q0, P0, P1)) ||
(s1==0 && PtInSegment(Q1, P0, P1)) ||
(t0==0 && PtInSegment(P0, Q0, Q1)) ||
(t1==0 && PtInSegment(P1, Q0, Q1))
一点P(x,y)在线段P0P1内判断函数PtInSegment(P, P0, P1)的算法设计思想为:
(1)当x0=x1时,y0<>y1(否则P0,P1为同一点),这时x=x0且y在y0,y1之间;
(2)当x0<>x1, y0=y1时,要求y=y0,且x在x0,x1之间;
(3)其它情况下要求三点共线,即det(P,P0,P1)=0,且x在x0,x1之间,y在y0,y1之间。
相应的程序为:
2.3 条件语句与循环语句的组织
代码中经常遇见条件语句与循环语句相交织的情况,有效地组织条件语句与循环语句,可以提高程序的效率。这种优化的策略涉及到程序的结构优化,应当属于全局优化的范畴。下面,我们通过一个例子来说明。
例 给定数组a[0...n-1],下面的函数当power=1、2、3时,分别计算数组中各元素的和、平方和及立方和。
int calc(int *a, int n, int power)
{
int i, s;
s = 0;
for (i=0; i < n; i++)
{
switch (power)
{
case 1: s+=a[i]; break;
case 2: s+=a[i]*a[i]; break;
case 3: s+=a[i]*a[i]*a[i]; break;
}
}
return s;
}
将这一程序结构改为下面的形式更有效:
int calc(int *a, int n, int power)
{
int i, s;
s = 0;
switch (power)
{
case 1:
for (i=0; i < n; i++)
s+=a[i];
break;
case 2:
for (i=0; i < n; i++)
s+=a[i]*a[i];
break;
case 3:
for (i=0; i < n; i++)
s+=a[i]*a[i]*a[i];
break;
}
return s;
}
在这一例子中将switch语句移入循环之外,可以避免在每次循环时进行的比较操作。在有些例子中,可能需要进行更大范围内的整合。