討論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語句移入循環之外,可以避免在每次循環時進行的比較操作。在有些例子中,可能需要進行更大範圍內的整合。
 

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