此片文章前半部分来自以下,为防止文章丢失,遂粘贴来。后部分为根据此文章的心得体会。C实现请下拉。
考研中,最近在复习数据结构。在PTA的数据结构与算法题目集(中文)上练习一下表达式转换。在将整体答对率从从0.16刷低到0.14,提交53次,历时6个半小时,从2017/9/13 17:26:08刷到2017/9/13 23:42:54,并询问同学无果后终于成功地解决此题。
原题如下。
----------------------------------------------------------------------------------------------------------------------------
7-20 表达式转换(25 分)
算术表达式有前缀表示法、中缀表示法和后缀表示法等形式。日常使用的算术表达式是采用中缀表示法,即二元运算符位于两个运算数中间。请设计程序将中缀表达式转换为后缀表达式。
输入格式:
输入在一行中给出不含空格的中缀表达式,可包含+
、-
、、
以及左右括号
()
,表达式不超过20个字符。(编者注:最后一个应该是’/’,原题就写错了)
输出格式:
在一行中输出转换后的后缀表达式,要求不同对象(运算数、运算符号)之间以空格分隔,但结尾不得有多余空格。
输入样例:
2+3(7-4)+8/4
输出样例:
2 3 7 4 - * + 8 4 / +
本人找到的官方给出的测试样例(根据陈越等的高教社版红皮《数据结构学习与实验指导》)
序号 | 输入 | 输出 | 说明 |
0 | 2+3*(7-4)+8/4 | 2 3 7 4 - * + 8 4 / + | 正常测试6种运算符 |
1 | ((2+3)4-(8+2))/5 | 2 3 + 4 * 8 2 + - 5 / | 嵌套括号 |
2 | 1314+25.512 | 1314 25.5 12 * + | 运算数超过1位整数且有非整数出现 |
3 | -2*(+3) | -2 3 | 运算数前有正负号 |
4 | 123 | 123 | 只有一个数字 |
但是实际上的测试样例根本不是像上面这样的。这也是为什么我明明样例0,1,4和所给输出完全一致(包括空格等)却一直提示格式错误得0分。
样例3和5的错误是意料之中,我本来就是只想做对样例0,1,4,测试一下自己的栈的使用来着。
提交时间 | 状态 | 分数 | 题目 | 编译器 | 耗时 | 用户 |
---|---|---|---|---|---|---|
2017/9/13 19:06:57 | 多种错误 | 0 | 7-20 | C++ (g++) | 2 ms |
测试点 | 提示 | 结果 | 耗时 | 内存 |
---|---|---|---|---|
0 | sample 6种运算符 | 格式错误 | 2 ms | 244KB |
1 | 嵌套括号 | 格式错误 | 2 ms | 244KB |
2 | 运算数超过1位整数且有非整数出现 | 答案错误 | 2 ms | 256KB |
3 | 运算数前有正负号 | 答案错误 | 2 ms | 240KB |
4 | 只有1个数字 | 格式错误 | 2 ms | 244KB |
错误代码(只考虑1位整数的情况)如下:
-
#include
-
#include
-
using
namespace
std;
-
int Prior(char ch){
-
int pri =
0;
-
switch(ch){
-
case
‘(’:
-
pri=
1;
break;
-
case
‘+’:
-
case
‘-’:
-
pri=
2;
break;
-
case
'’:
-
case
‘/’:
-
pri=
3;
break;
-
}
-
return pri;
-
}
-
void Zhong2Hou(char* src,char* str){
-
stack<
char> stc;
-
int i,j;
-
i=j=
0;
-
for(i=
0;src[i];i++){
//读取字符
-
if(src[i]>=
‘0’&&src[i]<=
‘9’){
//是操作数直接输出
-
str[j++]=src[i];
///printf("%c\n",str[j-1]);
-
}
else
if(src[i]
’(’){//是左括号直接入栈
-
stc.push(src[i]);
-
}
else
if(src[i]
’)’){
//是右括号则一直出栈到左括号并输出,左括号不输出
-
while(stc.top()!=
’(’){
-
str[j++]=stc.top();
///printf("%c\n",str[j-1]);
-
stc.pop();
-
}
-
stc.pop();
-
}
else{
//是其他操作符
-
if(stc.empty()){
//栈空直接入栈
-
stc.push(src[i]);
-
}
else
if(Prior(src[i])>Prior(stc.top())){
//优先级比栈顶的大直接入栈
-
stc.push(src[i]);
-
}
else{
//优先级比栈顶的小或等则一直出栈到空或大并输出,大时不输出
-
while(Prior(src[i])<=Prior(stc.top())){
-
str[j++]=stc.top();
///printf("%c\n",str[j-1]);
-
stc.pop();
-
if(stc.empty()){
-
break;
-
}
-
}
-
stc.push(src[i]);
//入栈该操作符
-
}
-
}
-
}
-
while(!stc.empty()){
//栈不空时全输出
-
str[j++]=stc.top();
///printf("%c\n",str[j-1]);
-
stc.pop();
-
}
-
str[j++]=
0;
//补0
-
}
-
int main(void)
-
{
-
char src[
100],str[
100];
-
scanf(
"%s",src);
-
Zhong2Hou(src,str);
-
int i;
-
for(i=
0;str[i+
1];i++){
-
putchar(str[i]);
putchar(
’ ‘);
-
}
-
putchar(str[i]);
-
return
0;
-
}
实际上,所有测试样例都是有多位整数的情况的。这就很坑了,要么彻底对,要么一点不对。
那么,为什么会提示格式错误而不是答案错误呢?
这是因为假设输入是
123
如果要求输出
123
而你输出
1 2 3
那么,这种情况会提示 “格式错误” 而不是 “答案错误” 。
按上面的错误代码输入123的确会产生如上所示的 “格式错误” 的输出。而测试样例0,1的格式错误同理。(仿佛看到了PAT和PTA系统背后的一点点判题代码(定义格式错误)的运行逻辑。)
下面这个是正确代码,刚好99行:
-
#include
-
#include
-
using
namespace
std;
-
///是否是数字
-
int IsNumber(char a){
-
return a>=
‘0’&&a<=
‘9’;
-
}
-
///从字符串start处开始提取操作数或操作符,并返回下一次提取的开端和提取的结果
-
int getop(char* src,int start,int &nextstart,char* token){
-
int i,j;
-
if(IsNumber(src[start])){
///是一个无符号数
-
for(i=
0;IsNumber(src[start+i])||src[start+i]
’.’;i++){///复制给token
-
token[i]=src[start+i];
-
}token[i]=
0;
-
nextstart=start+i;
///下一次提取的开端
-
return
1;
-
}
else
if((src[start]
’+’||src[start]
’-’)&&(start
0||src[start
-1]
’(’)){///是一个有符号数
-
if(src[start]
’-’){
-
token[
0]=src[start];j=
1;
-
}
else{
///去掉正号
-
j=
0;
-
}
-
for(i=
1;IsNumber(src[start+i])||src[start+i]
’.’;i++){///复制给token
-
token[j++]=src[start+i];
-
}token[j]=
0;
-
nextstart=start+i;
///下一次提取的开端
-
return
1;
-
}
else{
///不是数
-
token[
0]=src[start];
///复制操作符
-
token[
1]=
0;
-
nextstart=start+
1;
///下一次提取的开端
-
}
-
return
0;
-
}
-
///定义优先级。括号最低
-
int Prior(char ch){
-
int pri=
0;
-
switch(ch){
-
case
‘(’:
-
pri=
1;
break;
-
case
‘+’:
-
case
‘-’:
-
pri=
2;
break;
-
case
‘’:
-
case
‘/’:
-
pri=
3;
break;
-
}
-
return pri;
-
}
-
///表达式转换
-
void Zhong2Hou(char src,char* str){
-
stack<
char> stc;
-
int i,j,nexti;
char token[
100];
-
for(i=
0,j=
0;src[i];i=nexti){
-
int isnumber=getop(src,i,nexti,token);
///提取操作对象
-
if(isnumber){
///是个数
-
int k=
0;
///复制到str后
-
for(k=
0;token[k];k++){
-
str[j++]=token[k];
-
}
-
str[j++]=
’ ';
///用空格分隔
-
}
else
if(src[i]
’(’){
///左括号直接入栈
-
stc.push(src[i]);
-
}
else
if(src[i]==
’)’){
///右括号则弹出栈中全部操作符并复制到str后,直到左括号为止
-
while(stc.top()!=
’(’){
-
str[j++]=stc.top();stc.pop();
-
str[j++]=
’ ‘;
///用空格分隔
-
}
-
stc.pop();
///弹出左括号
-
}
else{
///是个一般的四则运算符
-
if(stc.empty()){
///空栈直接入栈
-
stc.push(src[i]);
-
}
else
if(Prior(src[i])>Prior(stc.top())){
///优先级高于栈顶则入栈
-
stc.push(src[i]);
-
}
else{
///否则弹出栈中元素直到优先级比栈顶高或空栈为止
-
while(!stc.empty()&&Prior(src[i])<=Prior(stc.top())){
-
str[j++]=stc.top();
-
stc.pop();
-
str[j++]=
’ ‘;
-
}
-
stc.push(src[i]);
///将该操作符入栈
-
}
-
}
-
}
-
while(!stc.empty()){
///弹出栈中全部元素
-
str[j++]=stc.top();
-
stc.pop();
-
str[j++]=
’ ';
-
}
-
str[j
-1]=
0;
///最后一个空格赋值为0
-
}
-
int main(void)
-
{
-
char src[
100]=
"",str[
100]=
"";
-
scanf(
"%s",src);
///输入
-
Zhong2Hou(src,str);
///转换
-
printf(str);
///打印
-
return
0;
-
}
起始点:
指向中缀串指针所指字符是数字,则是无符号数,
指向中缀串指针所指字符是正负号且所指字符是第一个或者所指字符的前一个字符是 ‘(’,则是有符号数。
否则所指为操作符。
结束点:
当是操作数时,直到下一个既不是数字也不是小数点的符号为止。
当是操作符时,直接加1(这里操作符都是1位)。
加减号是2,乘除号是3。为了在接下来遇到‘(’时省去判断栈顶是否是‘(’,将‘)’优先级定为1。优先级初始化为0。
中缀指针所指是操作数,则直接将之复制到后缀串尾,加空格。
中缀指针所指是左括号 ‘(’ ,则直接将之入栈。
中缀指针所指是右括号 ‘)’ ,则一直弹栈(并且复制到后缀串尾,加空格)到左括号 ‘(’ 为止,再将左括号弹栈。
中缀指针所指是一般的操作符‘+’、‘-’、‘’、‘/’,则
若栈为空,则直接入栈;
否则,若其优先级比栈顶元素高,则将之入栈;
否则,则一直弹栈(并且复制到后缀串尾,加空格)直到栈空或者其优先级比栈顶元素高为止(注意若此时遇到左括号,因为左括号我们设置的优先级很低,所以其优先级必比左括号高,可以停止)。然后将该操作符入栈。
遍历中缀表达式后,将栈中所剩操作符全部弹栈(并且复制到后缀串尾,加空格)。
最后根据题目要求,将最后一个空格赋值为结束符 ‘\0’。
----------------------------------------------------------------------------------------------------------------------------------------------------------
比如:2+3(7-4)+8/4
第一步,把每两个数之间的运算都加上括号(涉及视觉上并行地进行局部运算次序的判定)。((2+(3*(7-4)))+(8/4))
第二步,把中间的运算符放在所在的那一层括号右边(符号移动)。((2(3 (7 4)-)*)+(84)/)+
第三步,把括号全部去掉即为后缀表达式(符号消除或代替)。2 3 7 4 - * + 8 4 / +
转换为前缀表达式同理。
----------------------------------------------------------------------------------------------------------------------------------------------------------
其实这题的算法思想就跟栈没有什么关系,只是在解题过程中借用了一下栈这种数据结构来表达而已。大多数实际问题和算法题也都是这样的,本身解法与数据结构并无关系,只是在借助该类数据结构以一种简洁的方式来表示算法的思想,有没有这种类型的数据结构并不影响问题的解决,因为会在解决问题过程中自然而然的去发现和创造出这种数据结构(非常慢…)。不可以舍本逐末,本末倒置,只关心工具本身,要能明白问题的起源和终结问题的方法,这样才能掌握其本质。
----------------------------------------------------------------------------------------------------------------------------------------------------------
下一篇文章我们将使用创建二叉树+遍历二叉树的方法解决此题。
以上是在做这道题时主要参考的博客,版权地址在最上方。
上面博主的代码是c++实现,我自己在学习参考的过程中在PTA中使用c实现。
具体代码如下:(PTA已通过)
#include
#include
typedef char elemType;
typedef struct SNode
{
elemType postfixExpression[100];
int last;
}StackNode,*Stack;
void push_stack(Stack S,elemType x)
{
if(S->last+1==100)/*栈满*/
{
return;
}else{
S->postfixExpression[++S->last]=x;
return;
}
}
void pop_stack(Stack S,elemType *x)
{
if(S->last==-1)/*栈空*/
{
return;
}else{
*x=S->postfixExpression[S->last--];
return;
}
}
int stack_empty(Stack S)
{
if(S->last==-1)
{
return 1;
}else{
return 0;
}
}
int isnum(char x)/*判断是否是数字*/
{
if(x>='0'&&x<='9')
{
return 1;
}else{
return 0;
}
}
int main()
{
char *infixExpression;/*定义中缀*/
char *postfixExpression;/*定义后缀*/
infixExpression=(char *)malloc(50*sizeof(char));/*申请空间*/
postfixExpression=(char *)malloc(100*sizeof(char));/*申请空间*/
gets(infixExpression);/*输入中缀表达式*/
int i,j=0;
elemType oprand,operators;
Stack S;/*定义栈*/
S=(Stack)malloc(sizeof(StackNode));/*申请栈空间*/
S->last=-1;
for(i=0;infixExpression[i]!='\0';i++)/*遍历中缀*/
{
oprand=infixExpression[i];
switch(oprand)
{
/*这里正负有可能是运算符也有可能是正负号*/
case '+':
case '-':
/*先考虑是否是正负号的情况*/
if((oprand=='+'||oprand=='-')&&(!isnum(infixExpression[i-1])||i==0))/*说明是有符号数的+或-*/
{
if(oprand=='+'&&infixExpression[i-1]!=')')/*是正数不入栈也不输出,什么也不做直接下一个*/
{
break;
}else if(oprand=='-')/*遇到负号的话不像运算符比较入栈而是直接输出给后缀表达式*/
{
postfixExpression[j++]=oprand;
break;
}
}
/*是运算符的情况*/
while(!stack_empty(S))
{
if(S->postfixExpression[S->last]!='(')
{
postfixExpression[j++]=' ';/*为了迁就123和1254.6输出时字符间无空格这种情况 ,
所以对空格格式的处理放到里边来做,原则是每次入栈
一个运算符之后都输出一个空格,弹出一个运算符之前都输出一个空格,*/
pop_stack(S,&operators);
postfixExpression[j++]=operators;
}else
{
break;
}
}
push_stack(S,oprand);
postfixExpression[j++]=' ';/*格式需要*/
break;
case '(':
push_stack(S,oprand);
//postfixExpression[j++]=' ';/*格式不需要*/
break;
case '*':
case '/':
while(!stack_empty(S))
{
if(S->postfixExpression[S->last]=='*'||S->postfixExpression[S->last]=='/')
{
postfixExpression[j++]=' ';/*格式需要*/
pop_stack(S,&operators);
postfixExpression[j++]=operators;
}else
{
break;
}
}
push_stack(S,oprand);
postfixExpression[j++]=' ';/*格式需要*/
break;
case ')':
while(!stack_empty(S))
{
if(S->postfixExpression[S->last]!='(')
{
postfixExpression[j++]=' ';/*格式需要*/
pop_stack(S,&operators);
postfixExpression[j++]=operators;
}else
{
//postfixExpression[j++]=' ';/*格式不需要*/
pop_stack(S,&operators);
break;
}
}
break;
case '.':
postfixExpression[j++]=oprand;
break;
default:/*如果是有符号数*/
postfixExpression[j++]=oprand;
}
}
while(!stack_empty(S))/*将内容全部出栈*/
{
postfixExpression[j++]=' ';/*格式需要*/
pop_stack(S,&operators);
postfixExpression[j++]=operators;
}
postfixExpression[j++]='\0';/*设置结束标志,得到后缀表达式*/
int k=0;
for(k;postfixExpression[k]!='\0';k++)
{
printf("%c",postfixExpression[k]);
}
return 0;
}
下面是做这道题的一点体会:
实验课的时候系统的用栈做过表达式求值,中缀转后缀,后缀再求值,输出结果的char和int 转换…这道题虽然只让做了中缀转后缀,但是实现起来感觉更加琐碎,原因是第一意识是只做关于整数的求值。然后提交之后,显示四个格式错误,一个答案错误。需要多考虑的情况有一下几个方面:
这里实际上要想上面的博主学习**多多思考,勤于动脑。**根据给出的反馈去思考
**
1.怎么解决嵌套括号的问题?
A:这个不用单拎出来考虑,你在常规整数范围的中缀转后缀步骤中就能够解决括号嵌套的问题,不存在特殊情况。
2.怎么处理正负号?
A:正负号会和运算符加减混淆导致计算错误,所以一定要将两者分开。
运算符加减不过多叙述,这里困扰的是正负号。他不像加减一样入栈出栈,因为他不是运算符,不负责连接两个数字进行运算,他只属于一个数字,是这个数字的属性。那么处理方式就应该是:
如果是正号,则不对正号进行任何操作,不入栈不输出给后缀字符串。直接在switch里break;
如果是负号,不能忽略,不能入栈,正确的应该是直接输出给后缀字符串。(这很好理解的)
另一个细节就是怎么判断是是正负号而不是加减号:(oprand==’+’||oprand==’-’)&&(!isnum(infixExpression[i-1])||i==0)
根据这个符号前缀表达式的前一个elemtype进行判断,如果他前边的元素不是数字,那么代表他是正负号,或者他出现在前缀表达式的第一个元素位置。
3.怎么解决格式问题?题目要求不同对象(运算数、运算符号)之间以空格分隔,但结尾不得有多余空格。
A:第一次写的时候没有考虑到只有一个数字like1234和带有小数点Like123.456这样的情况,所以实现这个要求主要靠的是主函数里用for遍历在printf里控制空格。但是现在加上这两种特殊的明显不可以了,**想要在外部靠遍历中的细节操作实现格式 控制很难,所以考虑在前缀转换后缀的过程中,在处理每一个字符的时候就把格式控制搞定,输出的时候直接遍历输出不加空格就没问题。**这样在外部满足了连续数字和小数之无空格的要求,单个数字和运算符之间空格隔开就在内部转后缀的时候控制就可以。但具体实现控制也需要动一下脑子,想想表达式的样式特征,最终的解决办法就是:
每回在入栈运算符后输出一个空格给后缀字符串,出栈运算符前输出一个空格给后缀字符串。
之前之后是为什么?为什么要这么设?主要是考虑到,末尾不得有空格。如果在出栈后输出空格,想想有这么一种可能,就是最后前缀遍历完后,需要把所有栈中的运算符出栈,这样的话就会导致 末尾具有空格。
为什么在运算符出入栈上下工夫,而不考虑对char型0-9输出空格以满足要求?这个也很好理解,每个数字结束的标志是以运算符出现作为标志的like23+65.21-65显然在0-9上下工夫是无稽之谈。
代码编程要静下心,不要着急,不要急于求成。
成长若是完成,证明自动完成。:)