一、题目
◆3.21③ 假设表达式由单字母变量和双目四则运
算算符构成。试写一个算法,将一个通常书写形式
且书写正确的表达式转换为逆波兰式。
实现下列函数:
char *RPExpression(char *e);
/* 返回表达式e的逆波兰式 */
Stack是一个已实现的栈。
可使用的相关类型和函数:
typedef char SElemType; // 栈Stack的元素类型
Status InitStack(Stack &s);
Status Push(Stack &s, SElemType e);
Status Pop(Stack &s, SElemType &e);
Status StackEmpty(Stack s);
SElemType Top(Stack s);
-------------------------------------------------------------------------------------------------
二、思路
拿到题目,要做的第一件事情,就是搞懂题目究竟要我们做什么,很显然,题目中的关键字是“逆波兰式”,那么首先我们要搞懂这个概念。
所谓的逆波兰表示法(Reverse Polish notation,RPN,或逆波兰记法),是一种数学表达式方式,在逆波兰记法中,所有操作符置于操作数的后面,因此也被称为后缀表示法。逆波兰记法不需要括号来标识操作符的优先级。(摘自维基)
举个简单的例子,平常我们写的数学表达式a+b,就是一种中缀表达式,写成后缀表达式就是ab+。再举一个复杂的例子,中缀表达式(a+b)*c-(a+b)/e的逆波兰式是ab+c*ab+e/-。
在弄清楚概念以及题目的要求之后,接下来就要编写算法了。那么将一个表达式转换为逆波兰式的算法思想是什么呢?
(1)首先,需要分配2个栈,栈s1用于临时存储运算符(含一个结束符号),此运算符在栈内遵循越往栈顶优先级越高的原则;栈s2用于输入逆波兰式,为方便起见,栈s1需先放入一个优先级最低的运算符,在这里假定为'#';
(2)从中缀式的左端开始逐个读取字符x,逐序进行如下步骤:
1.若x是操作数,则分析出完整的运算数(在这里为方便,用字母代替数字),将x直接压入栈s2;
2.若x是运算符,则分情况讨论:
若x是'(',则直接压入栈s1;
若x是')',则将距离栈s1栈顶的最近的'('之间的运算符,逐个出栈,依次压入栈s2,此时抛弃'(';
若x是除'('和')'外的运算符,则再分如下情况讨论:
若当前栈s1的栈顶元素为'(',则将x直接压入栈s1;
若当前栈s1的栈顶元素不为'(',则将x与栈s1的栈顶元素比较,若x的优先级大于栈s1栈顶运算符优先级,则将x直接压入栈s1。否者,将栈s1的栈顶运算符弹出,压入栈s2中,直到栈s1的栈顶运算符优先级别低于(不包括等于)x的优先级,或栈s2的栈顶运算符为'(',此时再则将x压入栈s1;
(3)在进行完(2)后,检查栈s1是否为空,若不为空,则将栈中元素依次弹出并压入栈s2中(不包括'#');
(4)完成上述步骤后,栈s2便为逆波兰式输出结果。但是栈s2应做一下逆序处理,因为此时表达式的首字符位于栈底;
-------------------------------------------------------------------------------------------------
三、代码(C/C++)
1 char *RPExpression(char *e) 2 /* 返回表达式e的逆波兰式 */ 3 { 4 //栈s1用于存放运算符,栈s2用于存放逆波兰式 5 Stack s1,s2; 6 InitStack(s1); 7 InitStack(s2); 8 9 //假设字符'#'是运算级别最低的运算符,并压入栈s1中 10 Push(s1,'#'); 11 12 //p指针用于遍历传入的字符串,ch用于临时存放字符,length用于计算字符串长度 13 char *p=e,ch; 14 int length=0; 15 for(;*p!='\0';p++)//逐个字符访问 16 { 17 switch(*p) 18 { 19 //遇'('则直接入栈s1 20 case '(': 21 Push(s1,*p); 22 break; 23 //遇')'则将距离栈s1栈顶的最近的'('之间的运算符,逐个出栈,依次送入栈s2,此时抛弃'(' 24 case ')': 25 while(Top(s1)!='(') 26 { 27 Pop(s1,ch); 28 Push(s2,ch); 29 } 30 Pop(s1,ch); 31 break; 32 //遇下列运算符,则分情况讨论: 33 //1.若当前栈s1的栈顶元素是'(',则当前运算符直接压入栈s1; 34 //2.否则,将当前运算符与栈s1的栈顶元素比较,若优先级较栈顶元素大,则直接压入栈s1中, 35 // 否则将s1栈顶元素弹出,并压入栈s2中,直到栈顶运算符的优先级别低于当前运算符,然后再将当前运算符压入栈s1中 36 case '+': 37 case '-': 38 for(ch=Top(s1);ch!='#';ch=Top(s1)) 39 { 40 if(ch=='(') 41 { 42 break; 43 } 44 else 45 { 46 Pop(s1,ch); 47 Push(s2,ch); 48 } 49 } 50 Push(s1,*p); 51 length++; 52 break; 53 case '*': 54 case '/': 55 for(ch=Top(s1);ch!='#'&&ch!='+'&&ch!='-';ch=Top(s1)) 56 { 57 if(ch=='(') 58 { 59 break; 60 } 61 else 62 { 63 Pop(s1,ch); 64 Push(s2,ch); 65 } 66 } 67 Push(s1,*p); 68 length++; 69 break; 70 //遇操作数则直接压入栈s2中 71 default: 72 Push(s2,*p); 73 length++; 74 } 75 } 76 //若栈s1非空,则将栈中元素依次弹出并压入栈s2中 77 while(!StackEmpty(s1)&&Top(s1)!='#') 78 { 79 Pop(s1,ch); 80 Push(s2,ch); 81 } 82 //最后将栈s2输出,逆序排列成字符串; 83 char *result; 84 result=(char *)malloc(sizeof(char)*(length+1)); 85 result+=length; 86 *result='\0'; 87 result--; 88 for(;!StackEmpty(s2);result--) 89 { 90 Pop(s2,ch); 91 *result=ch; 92 } 93 ++result; 94 return result; 95 }
-------------------------------------------------------------------------------------------------
四、总结
对于实现逆波兰式算法,一开始不懂得概念的时候的确不知道如何入手,在摸清思路后,其实难度并不大,关键在于逻辑要清晰,而且要细心,写这段代码的时候很痛苦,共用了两天的时间(真的好菜)。
另摘录维基及度娘中关于实现逆波兰式的意义:(摘自百度)
为什么要将看似简单的中序表达式转换为复杂的逆波兰式?原因就在于这个简单是相对人类的思维结构来说的,对计算机而言中序表达式是非常复杂的结构。相对的,逆波兰式在计算机看来却是比较简单易懂的结构。因为计算机普遍采用的内存结构是栈式结构,它执行先进后出的顺序。
逆波兰式的意义:(摘自维基)
当有操作符时就计算,因此表达式并不是从右至左整体计算而是每次由中心向外计算一部分,这样在复杂运算中就很少导致操作符错误。
堆栈自动记录中间结果,这就是为什么逆波兰计算器能容易对任意复杂的表达式求值。与普通科学计算器不同,它对表达式的复杂性没有限制。
逆波兰表达式中不需要括号,用户只需按照表达式顺序求值,让堆栈自动记录中间结果;同样的,也不需要指定操作符的优先级。
逆波兰计算器中,没有“等号”键用于开始计算。
逆波兰计算器需要“确认”键用于区分两个相邻的操作数。
机器状态永远是一个堆栈状态,堆栈里是需要运算的操作数,栈内不会有操作符。
教育意义上,逆波兰计算器的使用者必须懂得要计算的表达式的含义。