讨论C语言常规优化策略——条件语句优化

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语句移入循环之外,可以避免在每次循环时进行的比较操作。在有些例子中,可能需要进行更大范围内的整合。
 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章