【資料結構】棧的應用 I :表示式求值
1. 介紹
表示式分為字首、中綴、字尾。字首表示式,也稱為波蘭表示式,其特點是將操作符置於運算元的前面。字尾表示式,也成為逆波蘭表示式,所有操作符置於運算元的後面。波蘭表示式、逆波蘭表示式均是由波蘭數學家Jan Łukasiewicz所提出的。中綴表示式將操作符放在運算元中間。字首表示式和字尾表示式相對於中綴表示式最大的不同是,去掉了表示運算優先順序的括號。
1.1 字首表示式求值
求值過程中會用到棧,用以儲存運算元和中間運算結果。
從右至左掃描字首表示式,進行如下操作:
(1)若遇到運算元,則運算元入棧;
(2)若遇到二元操作符,出棧兩個元素;若是一元操作符,出棧一個元素;進行操作符對應的運算,將運算結果入棧;
直至掃描完整個表示式,最後棧中只有一個元素,即為表示式的值。
1.2 字尾表示式求值
與字首表示式求值過程基本相同,不同的是從左至右掃描表示式。
1.3 中綴表示式求值
求值方法與下面的中綴轉字尾的演算法有點類似。中綴表示式有括號,並且操作符有運算優先順序,求值過程中會用到兩個棧:opnd棧和oprt棧。opnd棧用以儲存運算元或中間運算結果,oprt棧用以儲存操作符或'('。
從左至右掃描中綴表示式,每掃描到一個字元,做如下處理:
(1)若是運算元,則入opnd棧。
(2)若是操作符,則將其與oprt棧頂元素相比較,
①該操作符高於棧頂元素,說明該操作符的操作物件還沒被掃描到,直接將該操作符壓入oprt棧。
②該操作符的優先順序低於oprt棧頂元素,說明該操作符前面的式子應該先運算。將oprt棧進行出棧操作直至該操作符的優先順序高於棧頂元素(或棧頂元素為'(',或棧為空)。同時,opnd也對應地進行出棧操作,如果oprt棧頂元素是二元操作符,opnd出棧兩個元素;如果oprt棧頂元素是一元操作符,opnd出棧一個元素;然後,進行相應的運算,將運算結果壓入opnd棧。
(3)若遇到'(',直接將其壓入oprt棧。
(4)若遇到')',說明需要對該括號內的式子進行運算;oprt棧進行出棧操作直至棧頂元素是'(',並且在出棧的過程中做對應於oprt棧頂元素的運算,如(2)中②所描述。將運算結果壓入opnd棧。
掃描完中綴表示式後,oprt進行出棧操作直至棧為空,並做對應於oprt棧頂元素的運算,將運算結果壓入opnd棧。最後,opnd棧的棧頂元素即為中綴表示式的值。
2. 中綴與字尾之間的轉換
2.1 中綴轉字尾
將中綴表示式轉換為字尾表示式的經典演算法是Shunting yard演算法(也叫排程場演算法)[3],由Dijkstra引入,因其操作類似火車編組場而得名。為了做簡單的四則運算,這裡將Shunting yard演算法做了部分簡化。
用棧儲存操作符和左括號'('。
從左至右掃描中綴表示式,按如下情況進行處理:
(1)若遇到運算元,將運算元直接輸出;
(2)若遇到操作符,有以下兩類情況:
①操作符的運算優先順序高於棧頂元素,表明該操作符的下一個操作物件還沒被掃描到(針對的是二元操作符);將該操作符壓入棧。
②操作符的運算優先順序低於或等於棧頂元素,表明該操作符的兩個操作物件已經輸出了;進行出棧操作直至該操作符優先順序高於棧頂元素(或棧頂元素為'(',或棧為空);然後,將該操作符壓入棧。
(3)若遇到'(',直接將其壓入棧;
(4)若遇到')',表明該括號內的式子掃描完畢;進行出棧操作直至棧頂元素是'(',並且出棧的過程中輸出棧頂元素(相當於列印括號內的操作符)。
當掃描到中綴表示式的結尾時,將棧中元素出棧並輸出。所得到的輸出結果為所轉化的字尾表示式。
2. 2 字尾轉中綴
以操作符為非葉節點、運算元為葉節點的二叉樹,可以用來表示表示式,並且其前序、中序、後序遍歷分別為該表示式的字首、中綴、字尾表示法。表示式,無論是字首、中綴或是字尾,可以確定唯一一棵與之對應的二叉樹。這裡,操作符均指二元:'+','-','*','/'。由此易知,每一個操作符有左子樹和右子樹。我們稱左子樹所對應的表示式(字串的形式)為該操作符的左子串,與此相應地有右子串。
在中序遍歷該二叉樹時,遇到操作符時,對其子串是否加括號是一個非常棘手的問題,大概分四種情況考慮:
①若是'+',其左右子串均不需加括號。
②若是'-',僅需考慮其右子串是否加括號。如果右子串的根結點是'+'或'-'(也就是說右子串根結點的運算優先順序與'-'的相等),則需加括號;若是'*'或'/',則不需要加括號。
③若是'*',需考慮左右子串是否加括號。如果右子串的根結點是'+'或'-'(也就是說左子串根結點的運算優先順序小於'*'),則需加括號;若是'*'或'/',則不需要加括號。同樣地,對左子串的進行一樣的處理。
④若是'/',需考慮左右子串是否加括號。如果右子串的根結點是操作符,說明右子串是含有操作符的表示式,則需加括號。對左子串的處理方式與③相同,如果左子串的根結點是'+'或'-'(也就是說左子串根結點的運算優先順序小於'/'),則需加括號;若是'*'或'/',則不需要加括號。
所謂對子串加括號,是指在子串的末和尾分別加上'('和')'。
將字尾轉中綴有兩種方法:
方法一:由字尾表示式建立相應的二叉樹,再對二叉樹進行中序遍歷,所得結果即為中綴表示式。
方法二:用棧來實現。有兩個棧,一個是opnd棧,用以儲存子串或運算元;另一個是oprt棧,用以儲存是操作符的子串根節點,如果子串的根結點是運算元,不予儲存。注意到,在掃描字尾表示式時,子串的根結點都是最後才被掃描到。所以,也可以說oprt棧儲存的是子串的最後一個操作符。
從左至右掃描字尾表示式,
(1)若遇到運算元,直接入opnd棧。
(2)若遇操作符,從opnd棧取出兩個子串,棧頂是該操作符的右子串,棧頂的下一個是該操作符的左子串;對oprt棧,如果左右子串含操作符(也就是子串長度大於1),棧頂是左子串的最後一個操作符,棧頂的下一個是右子串的操作符。如果子串的長度大於1,說明該子串含有操作符,oprt棧做出棧操作。
(3)按照上面提及的規則,做加括號的處理。
(4)處理之後,操作符入oprt棧;左子串+操作符+右子串,構成新的子串,入opnd棧。
3. Referrence
[1] 維基百科,波蘭表示法。
[2] 維基百科,逆波蘭表示法。
4. 問題
4.1 POJ 3295
題目大意:對波蘭記法的字首表示式,判斷其是否為永真式。
有5個運算元p, q, r, s, and t,對應的取值情況有32種。為了取遍32種值,採用了網上程式碼的一個小技巧(i>>j)%2。
原始碼:
3295 | Accepted | 184K | 0MS | C++ | 1377B | 2013-09-24 11:02:18 |
#include<iostream>
#include<cstring>
#include<stack>
using namespace std;
stack<int>st;
int val[5];
bool flag;
char str[101];
void calculate(int len)
{
int i;
int temp1,temp2;
for(i=len-1;i>=0;i--)
{
switch(str[i])
{
case 'K':
{
temp1=st.top();
st.pop();
temp2=st.top();
st.pop();
st.push(temp1&&temp2);
break;
}
case 'A':
{
temp1=st.top();
st.pop();
temp2=st.top();
st.pop();
st.push(temp1||temp2);
break;
}
case 'N':
{
temp1=st.top();
st.pop();
st.push(!temp1);
break;
}
case 'C':
{
temp1=st.top();
st.pop();
temp2=st.top();
st.pop();
st.push(!temp1||temp2);
break;
}
case 'E':
{
temp1=st.top();
st.pop();
temp2=st.top();
st.pop();
st.push(temp1==temp2);
break;
}
default:
st.push(val[str[i]-'p']);
}
}
}
int main()
{
int i,j,len;
while (scanf("%s",&str)&&str[0]!='0')
{
len=strlen(str);
flag=true;
while(!st.empty()) //clear stack
st.pop();
for(i=0;i<32;i++)
{
for(j=0;j<5;j++)
val[j]=(i>>j)%2;
calculate(len);
if(!st.top())
{
flag=false;
break;
}
}
if(flag)
printf("tautology\n");
else
printf("not\n");
}
return 0;
}
4.2 POJ 2106
題目大意:對中綴記法的的布林表示式進行求值。
非運算'!'是一元運算,並且具有最高運算優先順序。輸入有諸如:"!!!F"的,需要考慮清楚。
沒考慮到"!!!F",RE了一次;還有一些情況沒考慮到,WA了幾次。邏輯關係一定要理清楚。
原始碼:
2106 | Accepted | 184K | 0MS | C++ | 1801B | 2013-09-30 17:56:35 |
#include <iostream>
#include <stack>
#include <map>
using namespace std;
stack<char>oprt;
stack<int>opnd;
map<char,int>priority;
int compute(int a,int b,char op)
{
if(op=='&')
return a&&b;
else if(op=='|')
return a||b;
}
int evaluate(char *in)
{
int i,temp1,temp2;
while(!oprt.empty())
oprt.pop();
while(!opnd.empty())
opnd.pop();
int len=strlen(in);
for(i=0;i<len;i++)
{
if(in[i]==' ')
continue;
else if(isupper(in[i]))
opnd.push(in[i]=='V');
else
{
switch(in[i])
{
case '&': case '|':
while(!oprt.empty()&&oprt.top()!='('&&priority[in[i]]<=priority[oprt.top()])
{
temp1=opnd.top();
opnd.pop();
if(oprt.top()=='!')
opnd.push((1+temp1)%2);
else
{
temp2=opnd.top();
opnd.pop();
opnd.push(compute(temp2,temp1,oprt.top()));
}
oprt.pop();
}
oprt.push(in[i]);
break;
case '!': case '(':
oprt.push(in[i]);
break;
case ')':
while(oprt.top()!='(')
{
temp1=opnd.top();
opnd.pop();
if(oprt.top()=='!')
opnd.push((1+temp1)%2);
else
{
temp2=opnd.top();
opnd.pop();
opnd.push(compute(temp2,temp1,oprt.top()));
}
oprt.pop();
}
oprt.pop();
break;
}
}
}
while(!oprt.empty())
{
temp1=opnd.top();
opnd.pop();
if(oprt.top()=='!')
opnd.push((1+temp1)%2);
else
{
temp2=opnd.top();
opnd.pop();
opnd.push(compute(temp2,temp1,oprt.top()));
}
oprt.pop();
}
return opnd.top();
}
int main()
{
int count=1;
char in[150];
priority['|']=1; priority['&']=2; priority['!']=3;
while(gets(in))
printf("Expression %d: %c\n",count++, evaluate(in)?'V':'F');
return 0;
}
4.3 POJ 1686
題目大意:判斷兩個表示式是否相等。
思路:將變數的值帶入計算:0~9按0~9處理,其他變數取ASCII碼值。若兩個表示式的值相等,即說明兩個表示式相等;反之,則否。中綴表示式求值太麻煩了,先將其轉化為字尾表示式,再求值。
不過,這種方法會誤判式子a+d與b+c相等。不過,好在測試資料沒這麼BT。
寫程式碼時,犯了好多低階錯誤。還有,scanf遇到空格、回車等會停止讀取,結束這一次的輸入。若輸入的字串帶空格、tab的話,用gets。
原始碼:
1686 | Accepted | 188K | 0MS | C++ | 1815B | 2013-09-25 17:32:35 |
#include <iostream>
#include <map>
#include <stack>
using namespace std;
map<char,int>priority;
/*transform the infix to the postfix*/
void transform(char *str)
{
stack<char>st1;
int i,j,len;
char temp[80];
len=strlen(str);
while (!st1.empty())
st1.pop();
for(i=0,j=0;i<len;i++)
{
if(isalnum(str[i]))
temp[j++]=str[i];
else
{
switch(str[i])
{
case '+': case '-': case'*':
while (!st1.empty()&&st1.top()!='('&&priority[str[i]]<=priority[st1.top()])
{
temp[j++]=st1.top();
st1.pop();
}
st1.push(str[i]);
break;
case '(':
st1.push(str[i]);
break;
case ')':
while (st1.top()!='(')
{
temp[j++]=st1.top();
st1.pop();
}
st1.pop();
break;
}
}
}
while (!st1.empty())
{
temp[j++]=st1.top();
st1.pop();
}
temp[j]='\0';
strcpy(str,temp);
}
/*calculate the result of a postfix expression*/
int calculate(char *s,int len)
{
int i;
int temp1,temp2;
stack<int>st2;
for(i=0;i<len;i++)
{
if(isdigit(s[i]))
st2.push(s[i]-'0');
else if(isalpha(s[i]))
st2.push(s[i]);
else
{
temp1=st2.top();
st2.pop();
temp2=st2.top();
st2.pop();
switch(s[i])
{
case '+':
st2.push(temp1+temp2);
break;
case '-':
st2.push(temp2-temp1);
break;
case '*':
st2.push(temp1*temp2);
break;
}
}
}
return st2.top();
}
int main()
{
int N,result1,result2;
char str1[80],str2[80];
priority['+']=1;
priority['-']=1;
priority['*']=2;
scanf("%d",&N);
getchar();
while (N--)
{
gets(str1);
transform(str1);;
result1=calculate(str1,strlen(str1));
gets(str2);
transform(str2);
result2=calculate(str2,strlen(str2));
if(result1==result2)
printf("YES\n");
else
printf("NO\n");
}
return 0;
}
4. 4 POJ 1400
題目大意:對中綴表示式去除多餘的括號。
現將中綴轉字尾,再將字尾轉中綴。
除錯、思考了一個晚上,一早起來靈感迸發,寫出來了。
原始碼:
1400 | Accepted | 256K | 704MS | C++ | 2586B | 2013-09-30 09:10:0 |
#include<iostream>
#include<string>
#include <stack>
#include <map>
using namespace std;
stack<char>oprt;
stack<string>opnd;
map<char,int>priority;
string infix2postfix(string in)
{
int i;
string post="";
while(!oprt.empty())
oprt.pop();
for(i=0;i<in.length();i++)
{
if(isalpha(in[i]))
post+=in[i];
else
{
switch(in[i])
{
case '+': case '-': case '*': case '/':
while(!oprt.empty()&&oprt.top()!='('&&priority[in[i]]<=priority[oprt.top()])
{
post+=oprt.top();
oprt.pop();
}
oprt.push(in[i]);
break;
case '(':
oprt.push(in[i]);
break;
case ')':
while(oprt.top()!='(')
{
post+=oprt.top();
oprt.pop();
}
oprt.pop();
break;
}
}
}
while(!oprt.empty())
{
post+=oprt.top();
oprt.pop();
}
return post;
}
string postfix2infix(string post)
{
int i;
string temp1,temp2;
char top_oprt,next_top;
while(!oprt.empty())
oprt.pop();
while(!opnd.empty())
opnd.pop();
for(i=0;i<post.length();i++)
{
if(isalpha(post[i]))
{
temp1="";
temp1+=post[i];
opnd.push(temp1);
}
else
{
temp1=opnd.top();
opnd.pop();
temp2=opnd.top();
opnd.pop();
if(temp1.length()>1)
{
top_oprt=oprt.top();
oprt.pop();
}
if(temp2.length()>1)
{
next_top=oprt.top();
oprt.pop();
}
switch(post[i])
{
case '-':
if(temp1.length()>1&&priority[top_oprt]==priority['-']) //add parenthese
{
temp1.insert(0,"(");
temp1.insert(temp1.length(),")");
}
break;
case '*':
if(temp1.length()>1&&priority[top_oprt]<priority['*'])
{
temp1.insert(0,"(");
temp1.insert(temp1.length(),")");
}
if(temp2.length()>1&&priority[next_top]<priority['*'])
{
temp2.insert(0,"(");
temp2.insert(temp2.length(),")");
}
break;
case '/':
if(temp1.length()>1)
{
temp1.insert(0,"(");
temp1.insert(temp1.length(),")");
}
if(temp2.length()>1&&priority[next_top]<priority['/'])
{
temp2.insert(0,"(");
temp2.insert(temp2.length(),")");
}
break;
}
oprt.push(post[i]);
temp2+=post[i];
temp2+=temp1;
opnd.push(temp2);
}
}
return opnd.top();
}
int main()
{
int N;
string in,post;
priority['+']=priority['-']=1;
priority['*']=priority['/']=2;
cin>>N;
while(N--)
{
cin>>in;
post=infix2postfix(in);
in=postfix2infix(post);
cout<<in<<endl;
}
return 0;
}