栈的应用——求解表达式

      栈有很多经典的应用,求解表达式便是重要的一个。首先要认识栈的基本特征,那就是FILO(first-in-last-out),先进后出。基本上符合先进后出这样一种模式的思想都能用栈来进行实现。那么现在回到我们这次的主题,求解表达式,为什么这会用到栈呢?换句话说,求解表达式哪里体现了先进后出的思想?我们来具体分析。

      我们如果要计算这样一个式子:2*3,你会脱口而出是6,计算机也知道是6。但再计算一个式子:2*(3+1),估计小学生也能脱口而出这是8,但计算机这时候就蒙了,因为它并不知道有优先级这种东西,也不知道怎么去算。计算机实在是很笨呀,那我们就要用一种方法来教会它怎么处理优先级的运算。

     一般来说,线性的运算更符合计算机的处理特点,如从左到右,或从右到左一个个扫描。而2*(3+1)这个式子先计算的是后面的小括号中的内容(因为小括号优先级最高),再处理前面的乘法,这就于计算机本身的处理特点不符,我们就是要通过一种方法来改写这个式子,使得能够让计算机更方便的进行运算,进而得出结果。

    这种改写的式子叫做后缀表达式,也称逆波兰表达式(是一个波兰的逻辑学家发明的,估计可能是他的名字太长了所以就以他的祖国命名了。这让我想到了有个数学定理叫中国剩余定理。)相对的,2*(3+1)这个式子叫做中缀表达式。这里的“中”和“后”描述的是运算符的位置,中缀表达式中的运算符在式子中,而后缀表达式的运算符在式子的最后面。如2*(3+1)的后缀表达式就为2 3 1 +*。注意到,后缀表达式中并没有括号。改写成了后缀表达式后,如果我们从数字和运算符(即这里的1和+)中间开始向左右扫描,取1和3两个操作数进行+的运算,得到4,把4再与2进行*运算,就得到了最后的结果。这就是线性的运算。

    那么现在问题的关键就是,如何把中缀表达式转换为后缀表达式,也就是如何得到左边的操作数的序列和右边的操作符的序列的问题。对于数字的序列,是特别简单的,直接按照原中缀表达式中的数字序列摆放就可以了。对于操作符的序列,我们要根据操作符的优先级来仔细处理:

     首先构造一个操作符栈。现在开始逐一扫描中缀表达式。前面说了,遇到数字就直接读入后缀表达式中,遇到操作符的话,第一个操作符由于操作符栈为空所以直接入栈。继续扫描,当扫描的操作符优先级大于栈顶元素的优先级时直接入栈,否则依次弹出优先级大于或等于所扫描到的操作符优先级的栈顶元素到后缀表达式中。是如果操作符优先级大于栈顶元素,说明在计算中它是处于稍早计算的一方,它直接进栈,后进先出,确保计算时优先计算它。而若优先级小于或等于栈顶元素,说明在计算中它是稍后计算的一方,所以它要先在栈中,所以弹出其他的栈顶元素。这恰恰体现了我开头所说的,先进后出的思想。如果遇到左括号就直接入栈,遇到右括号就一直弹出栈顶元素写入后缀表达式中直到遇见左括号为止(左括号不写入后缀表达式)因为左右括号起的作用就是标识两括号中间的内容进行优先运算。最后当扫描完毕中缀表达式后把操作符栈中的元素依次弹出至后缀表达式中(操作符栈非空的话)。

    至此,我们已经完成了由中缀表达式到后缀表达式的转换。接下来就是通过后缀表达式进行求值,这就很简单了。构造一个数栈读取后缀表达式中的值,然后一个个读取操作符,每读取到一个操作符就弹出数栈中的两个数进行运算,然后将运算结果入栈,作为新的栈顶元素。最后直到进行完最后一个操作符的运算,把最后一个栈内的元素输出。我们前面有提到过,通过后缀表达式进行求值的过程相当于是从数字与操作符的中间向两边扫描,所以,数字的计算起点是数字的最后一个,即是从左向右读取表达式时的最后一个最先进行计算,所以说,这里又体现了后进先出的栈的思想。

    下面就是具体的代码部分了。值得说明的是,栈这种数据结构在STL(Standard Template Library)标准模板库中已经定义好了,我们如果包含<stack>这个头文件就可以直接调用栈的相关函数进行操作。但是作为初学者,我们不应过度依赖这种成形的模板,而应更多的自己去实现,从而夯实自己的基础。其实实现一个栈并不难,从下面我的栈的相关的操作函数可以看出很多函数只有一条语句就完成了。

   还有就是,栈的应用还包括对字符串进行操作符匹配的检查,我发现正好可以用在这里作为检查输入的表达式是否左右括号匹配的检查,所以就加了这个功能。

  

#include<iostream>
#include<cctype>
#include<cmath>
#include<cstring>
using namespace std;
const int maxlen=100;
template<typename datatype>
class stack
{   private:
    int top;
   datatype array[maxlen];
   public:
   	stack();
   	void push(datatype);
   	datatype peek();
   	datatype pop();
   	bool empty();
};
template<typename datatype>
stack<datatype>::stack()//初始化栈 
{ top=-1;}
template<typename datatype>
void stack<datatype>::push(datatype x) //进栈操作 
{array[++top]=x;}
template<typename datatype>
 datatype stack<datatype>::peek()//查看栈顶元素值 
 {return array[top];}
 template<typename datatype>
datatype stack<datatype>::pop() //删除栈顶元素并返回其值 
{return array[top--];}
 template<typename datatype>
bool stack<datatype>::empty()//判断栈是否为空 
{return top==-1;}
bool check(char* infix)  //检查操作符是否匹配
{ stack<char> s;       //构造char类型栈存放操作符
 for(int i=0;i<strlen(infix);i++)
 {	 if(infix[i]=='(')
		 s.push(infix[i]);
	 if(infix[i]==')')
		 if(s.pop()!='(')
			 return false;}
		 return s.empty();}
bool isoperator(char ch)
{return ch=='+'||ch=='-'||ch=='*'||ch=='/'||ch=='%'||ch=='^'||ch=='('||ch==')';}
bool priority(char ch1,char ch2)//优先级比较
{ int mark1,mark2;
switch (ch1)
{case '(':mark1=0;break;//确保不会被弹出(遇到右括号除外)
 case '+':
 case '-':mark1=1;break;
 case '*':
 case '/':
 case '%':mark1=2;break;
 case '^':mark1=3;break;}
 switch (ch2)
{case '(':mark2=0;break;//确保不会被弹出(遇到右括号除外)
 case '+':
 case '-':mark2=1;break;
 case '*':
 case '/':
 case '%':mark2=2;break;
 case '^':mark2=3;break;}
 return mark1>mark2;
}
void trans(char* infix,char* postfix)//将中缀表达式转为后缀表达式 
{
  stack<char>s;   //构造char类型栈存放中缀表达式字符
  char ch;
  int j=0;
 int i=0;
 if(infix[0]=='-')
 {postfix[0]='-';i++;j++;}//对第一个操作数是负数的情况作特殊判断,直接读入后缀表达式
  while(i<strlen(infix))
  { ch=infix[i];
   if(ch=='(') {    //左括号直接入栈
   s.push(ch);i++;}
   else if(ch==')')    //右括号则把栈内元素弹出直到遇见左括号
   {while(s.peek()!='(')
   postfix[j++]=s.pop();
   s.pop();//弹出左括号
   i++;}
   else if(isoperator(ch))
   {if(s.empty())//如果栈为空,第一个操作符直接进入栈内 
   {s.push(ch);i++;}
   else if(priority(ch,s.peek()))  //如果栈不为空,则比较该操作符与栈顶元素的优先级,若栈顶元素优先级低于该操作符,则操作符直接入栈 
    {s.push(ch);i++;}
    else if(!priority(ch,s.peek()))    //若栈顶元素优先级高于或等于读入操作符优先级则依次弹出栈顶元素直到栈顶元素优先级低于读入操作符时停止弹出
     {
     while(!priority(ch,s.peek())&&!s.empty()) 
     postfix[j++]=s.pop();
     s.push(ch);//操作符进栈 
     i++;}
 } 
   else
  {
  while(!isoperator(ch)&&ch!='\0') //一位位的读取字符,为数字或者小数点时(即不是操作符时)写入后缀表达式中 
   {postfix[j++]=ch;ch=infix[++i];}
   postfix[j++]=' ';}//在数字的末尾加一个空格防止在处理后缀表达式时出现误读的情况,如12,不知道是数字12还是1和2两个数字
  }
     while(!s.empty())   //此时读取中缀表达式完毕,将栈中剩余的操作符输入到后缀表达式中   
     postfix[j++]=s.pop();
     postfix[j]='\0';
   }
   double calculate(char* postfix)//通过中缀表达式计算后缀表达式
   { stack<double>s;    //构造double型数栈存放操作数
    char ch;
    int i=0,flag=0,neg=0;
	double y=0,temp=10;
    double num,a,b;
	if(postfix[0]=='-')//如果第一个数为负号进行特殊处理,防止该负号被当作操作符(其实它也可以被当作操作符,只不过前面应该加个0)
	{neg=1;i++;}
	ch=postfix[i];
    while(ch!='\0')
    {if(isdigit(ch))
	{if(!flag) y=y*10+ch-'0';  //把字符转化为数值,这两个语句极其关键
	else{y+=(ch-'0')/temp;//flag是用来判断这是小数点前还是小数点后的数字
	temp*=10;}
	}
	 else if(ch=='.')flag=1;//如果数字的是小数,进行标记
     else if(ch==' ')
	 {   if(neg){s.push(-y);neg=0;}
		 else s.push(y);y=0;temp=10;flag=0;}
	 else //即遇到操作符
	 { b=s.pop();//这里a,b的顺序很重要,不能写反
	 a=s.pop();
	 switch(ch)
	 {case '+':num=a+b;break;
	  case '-':num=a-b;break;
	  case '*':num=a*b;break;
	  case '/':if(!b){cerr<<"除数不能为0哦!";exit(1);}num=a/b;break;
	  case '^':num=pow(a,b);break;
	  case '%':if((a-(int)a<1e-15||a-(int)a<-0.999999999999999)
			   &&(b-(int)b<1e-15||b-(int)b<-0.999999999999999))
			   num=int(a)%int(b);
			   else
			   {cerr<<"取余操作只能对两个整数进行哦!";exit(1);}break;
	 }
	 s.push(num);}
	 ch=postfix[++i];
	}
	 num=s.pop();//弹出最终的计算结果
	 return num;}
int main(){
    char infix[100];
    char postfix[100];
	char choice;
    do
	{cout<<"请输入你想要求解的表达式:"<<endl; 
    gets(infix);
	if(!check(infix)){cerr<<"你输入的表达式符号不匹配!"<<endl;exit(1);}
    trans(infix,postfix);
    double result=calculate(postfix);
    cout<<"表达式的值为:"<<result<<endl;
	cout<<"你想要再计算下一个式子吗?(Y/N)"<<endl;
	cin>>choice;
	getchar();//读取cin未读取的回车键以确保下次gets()函数能正常工作
	}
	while(choice=='y'||choice=='Y');
    return 0;
}

  

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值