中綴表達式求值問題
中綴表達式的求值問題是一個比較常見的問題之一,我們通常在編寫程序時,直接寫出表達式讓編譯器去處理,很少去關心編譯器是怎么對表達式進行求值的,今天我們來一起了解一下其中具體的原理和過程。
表達式一般來說有三種:前綴表達式、中綴表達式、后綴表達式,其中后綴表達式又叫做逆波蘭表達式。中綴表達式是最符合人們思維方式的一種表達式,顧名思義,就是操作符在操作數的中間。而前綴表達式和后綴表達式中操作符分別在操作數的前面和操作數的后面。舉個例子:
3+2
這個是最簡單的一個中綴表達式。而其等同的前綴表達式形式為+32,后綴表達式形式為32+。
那么一些朋友可能會問既然中綴表達式最符合人類的思維習慣,為什么還需要前綴表達式和后綴表達式?先看一個例子,假如在前面的表達式基礎上加一點東西:
3+2*5
此時的表達式很顯然,如果進行計算,則先計算2*5,最后計算加法。但是如果需要先計算加法運算呢?則必須加上括號,(3+2)*5。
而如果用后綴表達式來表示,則為 32+5*,那么該表達式的計算順序為3+2 —> (3+2)*5。
區別就在這里,后綴表達式不需要用括號就能表示出 整個表達式哪部分運算先進行。同理,前綴表達式也是如此。這種表達式正好最符合計算機的處理方式,因為后綴表達式和前綴表達式求值不需要考慮優先級的問題,計算機處理起來便簡單很多。
今天我們這里主要講解中綴表達式和后綴表達式(前綴表達式和后綴表達式很類似,就不做過多贅述),下面是講解大綱:
- 中綴表達式如何直接求值?
- 后綴表達式如何直接求值?
- 中綴表達式如何轉換為后綴表達式?
1.中綴表達式直接求值
對於中綴表達式求值來說,一般最常見的直接解決辦法就是利用棧,一個棧用來保存操作數,一個棧用來保存操作符。
為了簡便起見,暫時表達式中只考慮簡單的+,-,*,/運算,只有圓括號,並且都是整數。
假如有這樣一個表達式:((3+5*2)+3)/5+6/4*2+3
對於這樣一個表達式,如果讓你來設計操作數和操作符進棧的出棧的規則,你會怎么設計?
先不看這么復雜的表達式,考慮一下簡單點的,還是前面的3+2*5,那么很顯然先進行乘法運算,后進行加法運算,但是由於操作符在操作數中間,所以當一個操作符進操作符棧時,該操作符的兩個操作數並沒有都進入到操作數棧中,那么如何解決呢?只有在后面一個操作符進操作符棧時,前面的一個操作符所作用的兩個操作數才會全部進棧。比如3+2*5,棧的變化過程為:
操作數棧:3 操作數棧:3 操作數棧:3 2
操作符棧:空 操作符棧:+ 操作符棧:+
注意此時遇到操作符“*”,是不是需要彈出操作數棧中的兩個操作數進行運算呢,很顯然不是,因為乘法運算法比操作符棧的棧頂運算符優先級高,也就是說當前的操作符在“+”前進行運算,那么還需要將當前操作符壓棧,則變成:
操作數棧:3 2 操作數棧:3 2 5
操作符棧:+ * 操作符棧:+ *
此時到了表達式的結尾,既然棧頂的操作符的優先級比棧底的操作符的優先級高,那么可以取操作符棧的棧頂操作符和操作數棧的棧頂兩個元素進行計算,則得到2*5=10,(注意從操作數棧先彈出的操作數為右操作數)。此時得到10 ,則應該把10繼續壓到操作數棧中,繼續取操作符棧的棧頂操作符,依次進行下去,則當操作符棧為空時表示計算過程完畢,此時操作數棧中剩下的唯一元素便是整個表達式的值。
再換個例子:2*5+3,這個表達式跟前面表達式的結果雖然相同,但是操作數和操作符入棧和出棧的順序發生了很大變化:
操作數棧:2 操作數棧:2 操作數棧:2 5
操作符棧:空 操作符棧:* 操作符棧:*
此時遇到“+”,而操作符棧的棧頂操作符為“*”,棧頂操作符優先級更高,表示此時可以取操作符棧頂操作符進行運算,那么棧變成:
操作數棧:10 操作數棧:10 3
操作符棧:空 操作符棧:+
后面的過程跟前面一個例子類似。
如果復雜一點,比如包含有括號,連續的乘除法這些怎么處理呢?道理是一樣的,對於左括號直接入棧,碰到右括號,則一直將操作符退棧,直到碰到左括號,則括號中的表達式計算完畢。對於連續的乘除法,跟前面例子中處理過程類似。只需要記住一點:只有當前操作符的優先級高於操作符棧棧頂的操作符的優先級,才入棧,否則彈出操作符以及操作數進行計算直至棧頂操作符的優先級低於當前操作符,然后將當前操作符壓棧。當所有的操作符處理完畢(即操作符棧為空時),操作數棧中剩下的唯一一個元素便是最終的表達式的值。而操作符的優先級為:+和-優先級是一樣的,*和/優先級是一樣的,+、-的優先級低於*、/的優先級。
不過需要注意的是在求值之前需要對表達式進行預處理,去掉空格、識別 負號(區分“-”是作為減號還是負號),提取操作數等。
對於“-”的區分,主要判別方法為:
1)若前一個字符為‘(',則必定為負號;
2)若前一個字符為')'或者數字,則必定為減號;
3)若前面一個字符為其他運算符,如*,/,則必定是負號;
3)若前面沒有字符,即該字符為表達式的第一個字符,則必定是負號。
也就是說只有一種情況下,”-“是作為減號使用的,就是前一個字符為')'或者數字的時候。
如果判斷出“-”是作為負號使用的,這里我采用“#”來代替“-”,並將其作為一種運算(優先級最高)。比如:-3*2
我采取的做法是將"#"入棧,然后當遇到“*”時,由於棧頂操作符為"#",因此取#,然后取操作數棧的棧頂元素(只取一個)進行運算,然后再把結果壓棧。
下面是具體實現:
/*2014.5.6 測試環境: mingw*/ #include <iostream> #include <vector> #include <stack> #include <string> #include <cstdlib> #include <cctype> #include <cstring> using namespace std; vector<string> preParse(char *str) //對中綴表達式進行預處理,分離出每個token { vector<string> tokens; int len = strlen(str); char *p = (char *)malloc((len+1)*sizeof(char)); //注意不要用 char *p = (char *)malloc(sizeof(str))來申請空間 int i=0,j=0; while(i<len) //去除表達式中的空格 { if(str[i]==' ') { i++; continue; } p[j++] = str[i++]; } p[j]='\0'; j=0; len = strlen(p); while(j<len) { char temp[2]; string token; switch(p[j]) { case '+': case '*': case '/': case '(': case ')': { temp[0] =p[j]; temp[1] = '\0'; token=temp; tokens.push_back(token); break; } case '-': { if(p[j-1]==')'||isdigit(p[j-1])) //作為減號使用 { temp[0] =p[j]; temp[1] = '\0'; token=temp; tokens.push_back(token); } else //作為負號使用 { temp[0] ='#'; temp[1] = '\0'; token=temp; tokens.push_back(token); } break; } default: //是數字 { i = j; while(isdigit(p[i])&&i<len) { i++; } char *opd = (char *)malloc(i-j+1); strncpy(opd,p+j,i-j); opd[i-j]='\0'; token=opd; tokens.push_back(token); j=i-1; free(opd); break; } } j++; } free(p); return tokens; } int getPriority(string opt) { int priority; if(opt=="#") priority = 3; else if(opt=="*"||opt=="/") priority = 2; else if(opt=="+"||opt=="-") priority = 1; else if(opt=="(") priority = 0; return priority; } void calculate(stack<int> &opdStack,string opt) { if(opt=="#") //進行負號運算 { int opd = opdStack.top(); int result = 0-opd; opdStack.pop(); opdStack.push(result); cout<<"操作符:"<<opt<<" "<<"操作數:"<<opd<<endl; } else if(opt=="+") { int rOpd = opdStack.top(); opdStack.pop(); int lOpd = opdStack.top(); opdStack.pop(); int result = lOpd + rOpd; opdStack.push(result); cout<<"操作符:"<<opt<<" "<<"操作數:"<<lOpd<<" "<<rOpd<<endl; } else if(opt=="-") { int rOpd = opdStack.top(); opdStack.pop(); int lOpd = opdStack.top(); opdStack.pop(); int result = lOpd - rOpd; opdStack.push(result); cout<<"操作符:"<<opt<<" "<<"操作數:"<<lOpd<<" "<<rOpd<<endl; } else if(opt=="*") { int rOpd = opdStack.top(); opdStack.pop(); int lOpd = opdStack.top(); opdStack.pop(); int result = lOpd * rOpd; opdStack.push(result); cout<<"操作符:"<<opt<<" "<<"操作數:"<<lOpd<<" "<<rOpd<<endl; } else if(opt=="/") { int rOpd = opdStack.top(); opdStack.pop(); int lOpd = opdStack.top(); opdStack.pop(); int result = lOpd / rOpd; opdStack.push(result); cout<<"操作符:"<<opt<<" "<<"操作數:"<<lOpd<<" "<<rOpd<<endl; } } int evaMidExpression(char *str) //中綴表達式直接求值 { vector<string> tokens = preParse(str); int i=0; int size = tokens.size(); stack<int> opdStack; //存儲操作數 stack<string> optStack; //存儲操作符 for(i=0;i<size;i++) { string token = tokens[i]; if(token=="#"||token=="+"||token=="-"||token=="*"||token=="/") { if(optStack.size()==0) //如果操作符棧為空 { optStack.push(token); } else { int tokenPriority = getPriority(token); string topOpt = optStack.top(); int topOptPriority = getPriority(topOpt); if(tokenPriority>topOptPriority) { optStack.push(token); } else { while(tokenPriority<=topOptPriority) { optStack.pop(); calculate(opdStack,topOpt); if(optStack.size()>0) { topOpt = optStack.top(); topOptPriority = getPriority(topOpt); } else break; } optStack.push(token); } } } else if(token=="(") { optStack.push(token); } else if(token==")") { while(optStack.top()!="(") { string topOpt = optStack.top(); calculate(opdStack,topOpt); optStack.pop(); } optStack.pop(); } else //如果是操作數,直接入操作數棧 { opdStack.push(atoi(token.c_str())); } } while(optStack.size()!=0) { string topOpt = optStack.top(); calculate(opdStack,topOpt); optStack.pop(); } return opdStack.top(); } int main(int argc, char *argv[]) { char *str = "((3+5*2)+3)/5+(-6)/4*2+3"; cout<<evaMidExpression(str)<<endl; return 0; }
運行結果:
2.后綴表達式直接求值
由於后綴表達式不需要用括號來表示計算順序,因此處理起來比較簡單,具體的可以參照:
http://www.cnblogs.com/dolphin0520/p/3708587.html
3.中綴表達式如何轉為后綴
大部分數據結構教材在講述 棧的時候都會涉及到中綴表達式轉為后綴表達式的問題,因為這個是棧的典型應用之一。因此很多教材上都會利用棧來進行轉換,這里我們來討論一下最常見的兩種轉換思路和一種簡便的驗證方法。
- 利用二叉樹進行轉換
由於二叉樹本身結構的特殊性,使得我們可以利用它來很輕松地將中綴表達式轉變成后綴表達式,事實上,只要根據中綴表達式建立好相應的二叉樹之后,直接對二叉樹進行前序遍歷和后序遍歷便可得到前綴表達式和后綴表達式。在利用二叉樹來表示表達式時,用葉子節點來存儲操作數,用內部節點存儲操作符,比如這樣一個表達式3*5+5/2+(3+5)*2,表示成二叉樹的形式(注意其有等同的其他形式)就是:
其實講中綴表達式的過程轉變成二叉樹的形式是一個遞歸的過程,比如有一個表達式,其對應的的二叉樹的根節點必定是優先級最低的操作符(也就是說是整個表達式中最后進行的運算操作),然后再在操作符的左部分中找出最后進行的操作符作為根節點的左孩子,在操作符的右部分中找出最后進行的操作符作為根節點的右孩子,然后知道左部分或者右部分是單純的操作數,則作為葉子節點,直到整個二叉樹建立完畢。
下面是具體實現:
參考了一下這篇博文的實現,但是這篇博文沒有考慮減號作為負號使用的情況。
http://blog.csdn.net/ericming200409/article/details/5919883
/* 測試環境:VS2010 */ #include <iostream> #include <string> #include <cctype> #include <cstring> #include <cstdlib> #include <queue> using namespace std; typedef struct node { struct node *left; struct node *right; char *data; }BinTree; char * preProcess(char *str) //預處理,除去空格,將負號替代為# { int len = strlen(str); char *p = (char *)malloc(sizeof(char)*len); int i=0,j=0; while(i<len) //去除表達式中的空格 { if(str[i]==' ') { i++; continue; } p[j++] = str[i++]; } p[j]='\0'; j=0; len = strlen(p); while(j<len) { if(p[j]=='-') { if(!(p[j-1]==')'||isdigit(p[j-1]))) //作為減號使用 { p[j]='#'; } } j++; } return p; } /* 最后執行的操作符一定是在括號外面,也就是說brackets一定是等於0的, */ int indexOfOpt(char *str,int begin ,int end) //尋找最后執行的操作符的下標 { int i; int brackets=0; //所在括號層次 int index = -1; int existAddOrMinus = 0; int existMulOrDevide = 0; while(str[begin]=='('&&str[end]==')') //去除最外層的括號 { begin++; end--; } for(i=begin;i<=end;i++) { if(str[i]=='(') brackets++; else if(str[i]==')') brackets--; else if((str[i]=='+'||str[i]=='-')&&brackets==0) { index = i; existAddOrMinus = 1; //存在加減號 } else if((str[i]=='*'||str[i]=='/')&&brackets==0&&existAddOrMinus==0) { index = i; existMulOrDevide = 1; //存在乘除號 } else if(str[i]=='#'&&brackets==0&&existAddOrMinus==0&&existMulOrDevide==0) //用'#'代表負號 { index = i; } } return index; } BinTree * createBinTree(char *str,int begin,int end) { BinTree *p =(BinTree *)malloc(sizeof(BinTree));; int index = indexOfOpt(str,begin,end); cout<<"index:"<<index<<endl; if(index==-1) //表示只有操作數了 { while(str[begin]=='('&&str[end]==')') { begin++; end--; } p->data = (char *)malloc(sizeof(end-begin+2)); int i,j=0; for(i=begin;i<=end;i++) p->data[j++] = str[i]; p->data[j]='\0'; p->left = NULL; p->right = NULL; cout<<"操作數:"<<p->data<<endl; } else { p->data = (char*)malloc(2); p->data[0] = str[index]; p->data[1]='\0'; cout<<"操作符:"<<p->data<<endl; while(str[begin]=='('&&str[end]==')') { begin++; end--; } if(str[index]=='#') //是負號 { p->left = NULL; } else { p->left = createBinTree(str,begin,index-1); } p->right = createBinTree(str,index+1,end); } return p; } void preOrder(BinTree *root) { if(root!=NULL) { cout<<root->data<<" "; preOrder(root->left); preOrder(root->right); } } void inOrder(BinTree *root) { if(root!=NULL) { inOrder(root->left); cout<<root->data<<" "; inOrder(root->right); } } void postOrder(BinTree *root) { if(root!=NULL) { postOrder(root->left); postOrder(root->right); cout<<root->data<<" "; } } int main(void) { char *str = "((3+5*2)+3)/5+(-6)/4*2+3"; char *newStr = preProcess(str); cout<<newStr<<endl; BinTree *root=createBinTree(newStr,0,strlen(newStr)-1); inOrder(root); cout<<endl; postOrder(root); cout<<endl; system("pause"); return 0; }
上述代碼在VS2010下運行是沒有問題的,但是在gcc下編譯運行會崩潰,調試了很久沒發現原因(如果有哪位朋友知道原因的請麻煩告知)。測試結果:
- 利用棧進行轉換
利用棧進行轉換的思路其實跟前面直接對中綴表達式求值的過程類似,在這過程中需要一個棧用來保存操作符optStack,需要一個數組用來保存后綴表達式suffix[],然后從頭到尾掃描表達式
1)如果遇到操作符,則跟optStack的棧頂操作符比較優先級,如果大於棧頂操作符的優先級,則入棧,否則不斷取棧頂操作符加到suffix的末尾,直到棧頂操作符優先級低於該操作符,然后將該操作符入棧;
2)遇到操作數,直接加到suffix的末尾
3)遇到左括號,入棧;
4)遇到右括號,則依次彈出棧頂操作符加到suffix的末尾,直到遇到左括號,然后將左括號出棧。
具體實現:
/*2014.5.6 測試環境: mingw*/ #include <iostream> #include <vector> #include <stack> #include <string> #include <cstdlib> #include <cctype> #include <cstring> using namespace std; vector<string> preParse(char *str) //對中綴表達式進行預處理,分離出每個token { vector<string> tokens; int len = strlen(str); char *p = (char *)malloc((len+1)*sizeof(char)); //注意不要用 char *p = (char *)malloc(sizeof(str))來申請空間 int i=0,j=0; while(i<len) //去除表達式中的空格 { if(str[i]==' ') { i++; continue; } p[j++] = str[i++]; } p[j]='\0'; j=0; len = strlen(p); while(j<len) { char temp[2]; string token; switch(p[j]) { case '+': case '*': case '/': case '(': case ')': { temp[0] =p[j]; temp[1] = '\0'; token=temp; tokens.push_back(token); break; } case '-': { if(p[j-1]==')'||isdigit(p[j-1])) //作為減號使用 { temp[0] =p[j]; temp[1] = '\0'; token=temp; tokens.push_back(token); } else //作為負號使用 { temp[0] ='#'; temp[1] = '\0'; token=temp; tokens.push_back(token); } break; } default: //是數字 { i = j; while(isdigit(p[i])&&i<len) { i++; } char *opd = (char *)malloc(i-j+1); strncpy(opd,p+j,i-j); opd[i-j]='\0'; token=opd; tokens.push_back(token); j=i-1; free(opd); break; } } j++; } free(p); return tokens; } int getPriority(string opt) { int priority; if(opt=="#") priority = 3; else if(opt=="*"||opt=="/") priority = 2; else if(opt=="+"||opt=="-") priority = 1; else if(opt=="(") priority = 0; return priority; } vector<string> toSuffix(char *str) //轉變為后綴形式 { vector<string> tokens = preParse(str); int i=0; int size = tokens.size(); vector<string> suffix; //存儲后綴表達式 stack<string> optStack; //存儲操作符 for(i=0;i<size;i++) { string token = tokens[i]; if(token=="#"||token=="+"||token=="-"||token=="*"||token=="/") { if(optStack.size()==0) //如果操作符棧為空 { optStack.push(token); } else { int tokenPriority = getPriority(token); string topOpt = optStack.top(); int topOptPriority = getPriority(topOpt); if(tokenPriority>topOptPriority) { optStack.push(token); } else { while(tokenPriority<=topOptPriority) { optStack.pop(); suffix.push_back(topOpt); if(optStack.size()>0) { topOpt = optStack.top(); topOptPriority = getPriority(topOpt); } else break; } optStack.push(token); } } } else if(token=="(") { optStack.push(token); } else if(token==")") { while(optStack.top()!="(") { string topOpt = optStack.top(); suffix.push_back(topOpt); optStack.pop(); } optStack.pop(); } else //如果是操作數,直接入操作數棧 { suffix.push_back(token); } } while(optStack.size()!=0) { string topOpt = optStack.top(); suffix.push_back(topOpt); optStack.pop(); } return suffix; } int main(int argc, char *argv[]) { char *str = "((3+5*2)+3)/5+(-6)/4*2+3"; vector<string> suffix = toSuffix(str); int size = suffix.size(); for(int i=0;i<size;i++) cout<<suffix[i]<<" "; cout<<endl; return 0; }
測試結果:
- 簡便驗證辦法
最后一種辦法可以很快速地求出中綴表達式對應的前綴表達式和后綴表達式,就是添括號去括號法。
比如有表達式: (3+5*2)-2*3
先對每一個小部分添加括號: ((3+(5*2))-(2*3))
然后將每個操作符放到括號后面:((3(52)*)+(23)*)-
然后去括號:352*+23*-
便得到了后綴表達式,前綴表達式類似(只需把操作符放到括號前面即可)。