所谓逆波兰式表示法(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 应做一下逆序处理,因为此时表达式的首字符位于栈底;
此算法可以参考如下代码:
char *RPExpression(char *e)
/*返回表达式 e 的逆波兰式 */
{
//栈 s1用于存放运算符,栈 s2 用于存放逆波兰式
Stack s1,s2;
InitStack(s1);
InitStack(s2);
//假设字符 ‘#’ 是运算级别最低的运算符,并压入栈 s1 中
Push(s1,'#');
// p指针用于遍历传入的字符串, ch用于临时存放字符,length 用于计算字符串的长度
char *p=e,ch;
int length=0;
for(;*p!='\0';p++) // 逐个字符访问
{
switch(*p)
{
// 遇‘(’则直接入栈 s1
case '(':
Push(s1,*p);
break;
// 遇 ‘)’则将距离栈 s1 栈顶最近的‘(’之间的运算符,逐个出栈,依次送入栈s2, 此时抛弃‘(’
case ')':
while(Top(s1)!='(')
{
Push(s1,ch);
Push(s2,ch);
}
Pop(s1,ch);
break;
// 遇下列运算符,则分情况讨论:
// 1.若当前栈 s1 的栈顶元素式‘(’,则当前运算符直接压入栈 s1;
// 2.否则,将当前运算符与栈 s1 的栈顶元素比较,若优先级较栈顶元素大,则直接压入栈 s1 中,
// 否则将 s1 栈顶元素弹出,并压入栈 s2 中,直到栈顶运算符的优先级别低于当前运算符,然后将当前运算符压入栈 s1 中
case '+':
case '-':
for(ch=Top(s1);ch!='#';ch=Top(s1))
{
if(ch=='(')
{
break;
}
else
{
Pop(s1,ch);
Push(s2,ch);
}
}
Push(s1,*p);
length++;
break;
case '*':
case '/':
for(ch=Top(s1);ch!='#'&&ch!='-';ch=Top(s1))
{
if(ch=='(')
{
break;
}
else
{
Pop(s1,ch);
Push(s2,ch);
}
}
Push(s1,*p);
length++;
break;
case '*':
case '/':
for(ch=Top(s1);ch!='#'&&ch!='-';ch=Top(s1))
{
if(ch=='(')
{
break;
}
else
{
Pop(s1,ch);
Push(s2,ch);
}
}
Push(s1,*p);
length++;
break;
// 遇操作数则直接压入栈 s2 中
default:
Push(s2,*p);
length++;
}
}
// 若栈 s1 非空,则将栈中元素依次弹出并压入栈 s2 中
while(!StackEmpty(s1)&&Top(s1)!='#')
{
Pop(s1,ch);
Push(s2,ch);
}
// 最后将栈 s2 输出,逆序排列成字符串;
char *result;
result=(char *)malloc(sizeof(char)*(length+1));
result+=length;
*result='\0';
result--;
for(;!StackEmpty(s2);result--)
{
Pop(s2,ch);
*result=ch;
}
++result;
return result;
}
关于实现逆波兰的意义:
(百度) :为什么要将看似简单的中序表达死转换为复杂的逆波兰式? 原因就在于这个简单式相对人类的思维结构来说的,对计算机而言中序表达式式非常复杂的结构。相对的,逆波兰式在计算机看来却是比较简单易懂的结构。因为计算机普遍采用的内存结构是栈式结构,它执行先进后出的顺序。
(维基):当有操作符时就计算,因此表达式并不是从右至左整体计算而是每次由中心向外计算一部分,这样在复杂运算中就很少导致操作符错误。
堆栈自动记录中间结果,这就是为什么逆波兰计算器能容易对任意复杂的表达式求值。与普通计算器不同,他对表达式的复杂性没有限制。
逆波兰表达式不需要括号,用户只需按照表达式顺序求值,让堆栈自动记录中间结果,同样的,也不需要制定操作符的优先级。
逆波兰计算器中,没有“等号”键用于开始计算。
逆波兰计算器需要“确认”键用于区分两个相邻的操作数。
机器状态永远是一个堆栈状态,堆栈里是需要运算的操作数,栈内不会有操作符。
教育意义上,逆波兰计算器的使用者必须懂得要计算的表达式的含义。
参考: