1. 程式人生 > >【資料結構】棧的應用 I :表示式求值

【資料結構】棧的應用 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;
}