經典演算法-算術運算式求值
作者:蔣勇 http://www.kipway.com 轉載請保留此行
運算式的計算應用相當廣泛,比如電力調度系統中的計算遙測、車站票務系統中的票價類型計算公式等。
本文講述中置運算式轉換為後置運算式和後置運算式的求值演算法,並給出實現的C++原始碼,同時給出一個相當簡潔的堆棧C++模板類。
中綴運算式到尾碼運算式的轉換
要把運算式從中綴運算式的形式轉換成用尾碼標記法表示的等價運算式,必須瞭解操作符的優先順序和結合性。優先順序或者說操作符的強度決定求值順序;優先順序高
的操作符比優先順序低的操作符先求值。
如果所有操作符優先順序一樣,那麼求值順序就取決於它們的結合性。操作符的結合性定義了相同優先順序操作符組合的順序(從右至左或從左至右)。
轉換過程包括用下面的演算法讀入中綴運算式的運算元、操作符和括弧:
1. 初始化一個空堆棧,將結果字串變數置空。
2. 從左至右讀入中綴運算式,每次一個字元。
3. 如果字元是運算元,將它添加到結果字串。
4. 如果字元是個操作符,彈出(pop)操作符,直至遇見開括弧(opening parenthesis)、優先順序較低的操作符或者同一優先順序的右結合符號。把這個操作符壓入(push)堆棧。
5. 如果字元是個開括弧,把它壓入堆棧。
6. 如果字元是個閉括弧(closing parenthesis),在遇見開括弧前,彈出所有操作符,然後把它們添加到結果字串。
7. 如果到達輸入字串的末尾,彈出所有操作符並添加到結果字串。
尾碼運算式求值
對尾碼運算式求值比直接對中綴運算式求值簡單。在尾碼運算式中,不需要括弧,而且操作符的優先順序也不再起作用了。您可以用如下演算法對尾碼運算式求值:
1. 初始化一個空堆棧
2. 從左至右讀入尾碼運算式
3. 如果字元是一個運算元,把它壓入堆棧。
4. 如果字元是個操作符,彈出兩個運算元,執行恰當操作,然後把結果壓入堆棧。如果您不能夠彈出兩個運算元,尾碼運算式的文法就不正確。
5. 到尾碼運算式末尾,從堆棧中彈出結果。若尾碼運算式格式正確,那麼堆棧應該為空白。
資料結構
在這兩個演算法中都要利用堆棧。因此這裡首先給出一個堆棧的C++模板類,這個模板相當間接,只包含了運算式計算中必須要的功能。Pop Push Getsize 如下:
template <class T,int SIZE> class CArrayStackTemp
{
public:
CArrayStackTemp (){top= -1;}; //預設建構函式,構造一個空堆棧
~CArrayStackTemp (){};//解構函式
bool Push(T element); //入棧
bool Pop(T& element);//出棧
int GetSize(){return top+1;};//取元素個數
private:
T Buffer[SIZE];
int top;
};
template <class T, int SIZE> bool CArrayStackTemp<T, SIZE>:: Push (T element)
{
top++;
if (top>SIZE-1) {top--; return false;}//滿
Buffer[top]=element;
return true;
}
template <class T, int SIZE> bool CArrayStackTemp<T, SIZE>:: Pop (T& element)
{
if (top==-1) return false;//空
element=Buffer[top];
top--;
return true;
}
本文重在講述原理和實現,因此以下演算法只包含了+-*/()的處理,運算元自動轉換為double型浮點數。是y=f(x)函數的計算,因此運算式中支援'X'字元,不分大小寫,比如:y=(x-5)+2/(x+1),下面的演算法代碼就是計算y值。
中綴運算式到尾碼運算式的轉碼
LPCTSTR lpszS:運算式字串
CString &szD:結果
return =0:ok
int g_Mid2Behind(LPCTSTR lpszS,CString &szD)//中置->後置 只支援() + - * / 和X變數
{
CArrayStackTemp <char, 256> stk;//堆棧
CString szs=lpszS,szt="";
szs.TrimLeft(); szs.TrimRight();szs.MakeUpper();//大寫
int i,len=szs.GetLength();
char c,cc,buf[256];;
for(i=0;i<len;i++)
{
c=szs[i];
if((c>'9')&&(c!='X')) return 1;//含有非法字元
if((c==' ')||(c=='/t')||(c==0x0d)||(c==0x0a)) continue;
szt+=c;
}
len=szt.GetLength();
if(len > 255) return 2;//太長
sprintf(buf,"%s",szt);
buf[len]=0;
char *p=(char *)buf;
szt=""; szD="";
bool bret;
while(*p != 0)
{
c=*p;
if( ((c>='0')&&(c<='9'))|| (c=='X')|| (c=='.') ){szt+=c;p++;continue;}
else
{
if(!szt.IsEmpty())//運算元,添加到結果字串
{
len=szt.GetLength();
for(i=0;i<len;i++) szD+=szt[i];
szt="";
}
if((c == '+')||(c=='-'))
{
do
{ if(!stk.Pop(cc)) cc=0;
if((cc!=0)&&(cc!='(')) szD += cc;
}while((cc!=0)&&(cc!='('));
if(cc=='(') stk.Push(cc);
stk.Push(c);
}
else if((c == '*')||(c=='/'))
{
do
{ if(!stk.Pop(cc)) cc=0;
if((cc!=0)&&(cc!='(')&&(cc!='+')&&(cc!='-')) szD += cc;
}while((cc!=0)&&(cc!='(')&&(cc!='+')&&(cc!='-'));
if((cc=='(')||(cc=='-')||(cc=='+')) stk.Push(cc);
stk.Push(c);
}
else if(c == '(') stk.Push(c);
else if(c == ')')
{
do
{ if(!stk.Pop(cc)) cc=0;
if((cc!=0)&&(cc!='(')) szD += cc;
}while((cc!=0)&&(cc!='('));
if(cc=='(') stk.Push(cc);
}
else return 3;//操作符錯誤
}
p++;
}
if(!szt.IsEmpty())//運算元,添加到結果字串
{
len=szt.GetLength();
for(i=0;i<len;i++) szD+=szt[i];
szt="";
}
while(stk.GetSize())
{
bret=stk.Pop(cc);
if((bret==true)&&(cc!='(')) szD += cc;
}
return 0;
}
尾碼運算式求值代碼
LPCTSTR lpszParse:後置運算式
double xval:變數X的值
double *pResult:計算結果
無錯時返回TRUE
BOOL CalBehindParse(LPCTSTR lpszParse,double xval,double *pResult)
{
char c;
const char *pc=lpszParse;
double v1,v2;
CArrayStackTemp <double, 256> stk;//堆棧
CString szt="";
while(*pc != NULL)
{
c=*pc;
if(c=='X') stk.Push(xval);
else if( ((c>='0')&&(c<='9')) || (c=='.') ) {szt+=c;pc++;continue;}
else
{
if(!szt.IsEmpty()){ stk.Push(atof(szt));szt=""; }
if(!stk.Pop(v2)) return FALSE;
if(!stk.Pop(v1)) return FALSE;
if(c=='+') stk.Push(v1+v2);
else if(c=='-') stk.Push(v1-v2);
else if(c=='*') stk.Push(v1*v2);
else if(c=='/') stk.Push(v1/v2);
else return FALSE;//非法運算子
}
pc++;
}
if(!szt.IsEmpty()){ stk.Push(atof(szt));szt=""; }
if(stk.GetSize()!=1) return FALSE;
return stk.Pop(*pResult);
}
把 CalBehindParse()稍作修改就可以得到後置運算式正確性檢查的函數,如下:
BOOL CheckBehindParse(LPCTSTR lpszParse)//檢查後置運算式
{
char c;
const char *pc=lpszParse;
double v1,v2;
CArrayStackTemp <double, 256> stk;
CString szt="";
while(*pc != NULL)
{
c=*pc;
if(c=='X') stk.Push(1);
else if( ((c>='0')&&(c<='9')) || (c=='.') ) {szt+=c;pc++;continue;}
else
{
if(!szt.IsEmpty()){stk.Push(atof(szt)); szt=""; }
if(!stk.Pop(v2)) return FALSE;
if(!stk.Pop(v1)) return FALSE;
if((c=='+')||(c=='-')||(c=='*')||(c=='/')) stk.Push(1);
else return FALSE;//非法運算子
}
pc++;
}
if(stk.GetSize()!=1) return FALSE;
return TRUE;
}
綜合以上這些函數,可以得到中置運算式的直接計算和檢查函數:
BOOL CheckParse(LPCTSTR lpszParse)//檢查中置運算式是否正確
{
CString szb;
if(0!=g_Mid2Behind(lpszParse,szb))
return FALSE;
if(szb.IsEmpty()) return TRUE;
return CheckBehindParse(szb);
}
BOOL CalParse(LPCTSTR lpszParse,double dblVar,double *pResult)//計算中置運算式
{
CString szb;
if(0!=g_Mid2Behind(lpszParse,szb))
return FALSE;
if(szb.IsEmpty()){*pResult=0; return TRUE;}
return CalBehindParse(szb,dblVar,pResult);
}
你可以用下面的測試例子去測試上面的函數:
double dblResult;
BOOL bret=CalParse("X / 2 * (X + 2.5)",2,&dblResult);//計算中置運算式
註:本文的代碼在windows xp,VC++6 + vcsp5環境下驗證通過。
-----------------------------------------------------------------------------------------------
運算式求值的經典演算法
編寫代碼對算術運算式求值的經典方法由 Donald Knuth 描述於 1962 年(請參閱 參考資料)。Knuth 將此概括為三個步驟:
- 對中綴運算式進行文法分析
- 中綴運算式到尾碼運算式的轉換
- 對尾碼運算式求值
注意到我們談到的這個經典演算法有些簡化:算術運算式只包含運算元、二元操作符和一種括弧。此外,對於每個運算元和操作符,只用單個字元表示,使文法分析直觀。
運算式標記法
算術運算式中最常見的標記法形式有 中綴、首碼和 尾碼標記法。中綴標記法是書寫運算式的常見方式,而首碼和尾碼標記法主要用於電腦科學領域。
中綴標記法
中綴標記法是算術運算式的常規標記法。稱它為 中綴標記法是因為每個操作符都位於其運算元的中間,這種標記法只適用於操作符恰好對應兩個運算元的時候(在操作符是二元操作符如加、減、乘、除以及模數的情況下)。對以中綴標記法書寫的運算式進行文法分析時,需要用括弧和優先規則排除多義性。
Syntax: operand1 operator operand2 Example: (A+B)*C-D/(E+F)
|
首碼標記法
首碼標記法中,操作符寫在運算元的前面。這種標記法經常用於電腦科學,特別是編譯器設計方面。為紀念其發明家 ― Jan Lukasiewicz(請參閱 參考資料),這種標記法也稱 波蘭標記法。
Syntax : operator operand1 operand2 Example : -*+ABC/D+EF
|
尾碼標記法
在尾碼標記法中,操作符位於運算元後面。尾碼標記法也稱 逆波蘭標記法(reverse Polish notation,RPN),因其使運算式求值變得輕鬆,所以被普遍使用。
Syntax : operand1 operand2 operator Example : AB+C*DEF+/-
|
首碼和尾碼標記法有三項公用特徵:
- 運算元的順序與等價的中綴運算式中運算元的順序一致
- 不需要括弧
- 操作符的優先順序不相關
中綴運算式到尾碼運算式的轉換
要把運算式從中綴運算式的形式轉換成用尾碼標記法表示的等價運算式,必須瞭解操作符的優先順序和結合性。 優先順序或者說操作符的強度決定求值順序;優先順序高的操作符比優先順序低的操作符先求值。 如果所有操作符優先順序一樣,那麼求值順序就取決於它們的 結合性。操作符的結合性定義了相同優先順序操作符組合的順序(從右至左或從左至右)。
Left associativity : A+B+C = (A+B)+C Right associativity : A^B^C = A^(B^C)
|
轉換過程包括用下面的演算法讀入中綴運算式的運算元、操作符和括弧:
- 初始化一個空堆棧,將結果字串變數置空。
- 從左至右讀入中綴運算式,每次一個字元。
- 如果字元是運算元,將它添加到結果字串。
- 如果字元是個操作符,彈出(pop)操作符,直至遇見開括弧(opening parenthesis)、優先順序較低的操作符或者同一優先順序的右結合符號。把這個操作符壓入(push)堆棧。
- 如果字元是個開括弧,把它壓入堆棧。
- 如果字元是個閉括弧(closing parenthesis),在遇見開括弧前,彈出所有操作符,然後把它們添加到結果字串。
- 如果到達輸入字串的末尾,彈出所有操作符並添加到結果字串。
尾碼運算式求值
對尾碼運算式求值比直接對中綴運算式求值簡單。在尾碼運算式中,不需要括弧,而且操作符的優先順序也不再起作用了。您可以用如下演算法對尾碼運算式求值:
- 初始化一個空堆棧
- 從左至右讀入尾碼運算式
- 如果字元是一個運算元,把它壓入堆棧。
- 如果字元是個操作符,彈出兩個運算元,執行恰當操作,然後把結果壓入堆棧。如果您不能夠彈出兩個運算元,尾碼運算式的文法就不正確。
- 到尾碼運算式末尾,從堆棧中彈出結果。若尾碼運算式格式正確,那麼堆棧應該為空白。