在電腦中進行算術運算式的計算是通過棧來實現的。這一節首先討論算術運算式的兩種表示方法,即中綴標記法和尾碼標記法,接著討論尾碼運算式求值的演算法,最後討論中綴運算式轉換為尾碼運算式的演算法。
1. 算術運算式的兩種表示
通常書寫的算術運算式是由運算元(又叫運算對象或運算量)和運算子以及改變運算次序的圓括弧串連而成的式子。運算元可以是常量、變數和函數,同時還可以是運算式。運算子包括單目運算子和雙目運算子兩類,單目運算子只要求一個運算元,並被放在該運算元的前面,雙目運算子要求有兩個運算元,並被放在這兩個運算元的中間。單目運算子為取正’+’和取負’-’,雙目運算子有加’+’,減’-’,乘’*’和除’/’等。為了簡便起見,在我們的討論中只考慮雙目運算子。
如對於一個算術運算式2+5*6,乘法運算子’*’的兩個運算元是它兩邊的5和6;對於加法運算子’+’的兩個運算元,一個是它前面的2,另一個是它後面的5*6的結果即30。我們把雙目運算子出現在兩個運算元中間的這種習慣表示叫做算術運算式的中綴表示,這種算術運算式被稱為中綴算術運算式或中綴運算式。
中綴運算式的計算比較複雜,它必須遵守以下三條規則:
(1) 先計算括弧內,後計算括弧外;
(2) 在無括弧或同層括弧內,先進行乘除運算,後進行加減運算,即乘除運算的優先順序高於加減運算的優先順序;
(3) 同一優先順序運算,從左向右依次進行。
從這三條規則可以看出,在中綴運算式的計算過程中,既要考慮括弧的作用,又要考慮運算子的優先順序,還要考慮運算子出現的先後次序。因此,各運算子實際的運算次序往往同它們在運算式中出現的先後次序是不一致的,是不可預測的。當然憑直觀判別一個中綴運算式中哪個運算子最先算,哪個次之,……,哪個最後算並不困難,但通過電腦處理就比較困難了,因為電腦只能一個字元一個字元地掃描,要想得到哪一個運算子先算,就必須對整個中綴運算式掃描一遍,一個中綴運算式中有多少個運算子,原則上就得掃描多少遍才能計算完畢,這樣就太浪費時間了,顯然是不可取的。
那麼,能否把中綴算術運算式轉換成另一種形式的算術運算式,使計算簡單化呢? 回答是肯定的。波蘭科學家盧卡謝維奇(Lukasiewicz)很早就提出了算術運算式的另一種表示,即尾碼表示,又稱逆波蘭式,其定義是把運算子放在兩個運算對象的後面。採用尾碼表示的算術運算式被稱為尾碼算術運算式或尾碼運算式。在尾碼運算式中,不存在括弧,也不存在優先順序的差別,計算過程完全按照運算子出現的先後次序進行,整個計算過程僅需一遍掃描便可完成,顯然比中綴運算式的計算要簡單得多。例如,對於尾碼運算式12!4!-!5!/,其中’!’字元表示成分之間的空格,因減法運算子在前,除法運算子在後,所以應先做減法,後做除法;減法的兩個運算元是它前面的12和4,其中第一個數12是被減數,第二個數4是減數;除法的兩個運算元是它前面的12減4的差(即8)和5,其中8是被除數,5是除數。
中綴算術運算式轉換成對應的尾碼算術運算式的規則是:把每個運算子都移到它的兩個運算對象的後面,然後刪除掉所有的括弧即可。
例如,對於下列各中綴運算式:
(1) 3/5+6
(2) 16-9*(4+3)
(3) 2*(x+y)/(1-x)
(4) (25+x)*(a*(a+b)+b)
對應的尾碼運算式分別為:
(1) 3!5!/!6!+
(2) 16!9!4!3!+!*!-
(3) 2!x!y!+!*!1!x!-!/
(4) 25!x!+!a!a!b!+!*!b!+!*
2. 尾碼運算式求值的演算法
尾碼運算式的求值比較簡單,掃描一遍即可完成。它需要使用一個棧,假定用S表示,其元素類型應為運算元的類型,假定為浮點型float,用此棧儲存尾碼運算式中的運算元、計算過程中的中間結果以及最後結果。假定一個尾碼算術運算式以字元’@’作為結束符,並且以一個字串的方式提供。尾碼運算式求值演算法的基本思路是:把包含尾碼算術運算式的字串定義為一個輸入字串流對象,每次從中讀入一個字元(空格作為資料之間的分隔字元,不會被作為字元讀入)時,若它是運算子,則表明它的兩個運算元已經在棧S中,其中棧頂元素為運算子的後一個運算元,棧頂元素的下一個元素為運算子的前一個運算元,把它們彈出後進行相應運算即可,然後把運算結果再壓入棧S中;否則,讀入的字元必為運算元的最高位元字,應把它重新送回輸入資料流中,然後把下一個資料作為浮點數輸入,並把它壓入到棧S中。依次掃描每一個字元(對於浮點數只需掃描它的最高位並一次輸入整個浮點數)並進行上述處理,直到遇到結束符’@’為止,表明尾碼運算式計算完畢,最終結果儲存在棧中,並且棧中僅存這一個值,把它彈出返回即可。具體演算法描述為:float Compute(char* str)
// 計算由str字串所表示的尾碼運算式的值,
// 運算式要以'@'字元結束。
...{
Stack S; // 用S棧儲存運算元和中間計算結果
InitStack(S); // 初始化棧
istrstream ins(str); // 把str定義為輸入字串流對象ins
char ch; // 用於輸入字元
float x; // 用於輸入浮點數
ins>>ch; // 從ins流對象(即str字串)中順序讀入一個字元
while(ch!='@')
...{ // 掃描每一個字元並進行相應處理
switch(ch)
...{
case '+':
x=Pop(S)+Pop(S);
break;
case '-':
x=Pop(S); // Pop(S)彈出減數
x=Pop(S)-x; // Pop(S)彈出的是被減數
break;
case '*':
x=Pop(S)*Pop(S);
break;
case '/':
x=Pop(S); // Pop(S)彈出除數
if(x!=0.0)
x=Pop(S)/x; // Pop(S)彈出的是被除數
else ...{ // 除數為0時終止運行
cerr<<"Divide by 0!"< exit(1);
}
break;
default: // 讀入的必為一個浮點數的最高位元字
ins.putback(ch); // 把它重新回送到輸入資料流中
ins>>x; // 從字串輸入資料流中讀入一個浮點數
}
Push(S,x); // 把讀入的一個浮點數或進行相應運算
// 的結果壓入到S棧中
ins>>ch; // 輸入下一個字元,以便進行下一輪迴圈處理
}
if(!StackEmpty(S))
...{ // 若棧中僅有一個元素,則它是尾碼運算式的值,否則為出錯
x=Pop(S);
if(StackEmpty(S))
return x;
else ...{
cerr<<"expression error!"< exit(1);
}
}
else ...{ // 若最後棧為空白,則終止運行
cerr<<"Stack is empty!"< exit(1);
}
}
此演算法的已耗用時間主要花在while迴圈上,它從頭到尾掃描尾碼運算式中的每一個資料(每個運算元或運算子均為一個資料),若尾碼運算式由n個資料群組成,則此演算法的時間複雜度為O(n)。此演算法在運行時所佔用的臨時空間主要取決於棧S的大小,顯然,它的最大深度不會超過運算式中運算元的個數,因為運算元的個數與運算子(假定把’@’也看作為一個特殊運算子,即結束運算子)的個數相等,所以此演算法的空間複雜度也同樣為O(n)。
假定一個字串a為:
char a[30]="12 3 20 4 / * 8 - 6 * +@";
對應的中綴算術運算式為12+(3*(20/4)-8)*6@,則使用如下語句調用上述函數得到的輸出結果為54。
cout< 在進行這個尾碼算術運算式求值的過程中,從第四個運算元入棧開始,每處理一個運算元或運算子後,棧S中儲存的運算元和中間結果的情況4-4所示。