数据结构之栈----PTA题目7-20表达式转换(中缀转后缀)

一、前言

考研中,最近在复习数据结构。在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 / +
----------------------------------------------------------------------------------------------------------------------------

三、测试样例分析

1.测试样例

本人找到的官方给出的测试样例(根据陈越等的高教社版红皮《数据结构学习与实验指导》)

表达式转换测试用例
序号 输入 输出 说明
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.5*12 1314 25.5 12 * + 运算数超过1位整数且有非整数出现
3 -2*(+3) -2 3 * 运算数前有正负号
4 123 123 只有一个数字










但是实际上的测试样例根本不是像上面这样的。这也是为什么我明明样例0,1,4和所给输出完全一致(包括空格等)却一直提示格式错误得0分。

样例3和5的错误是意料之中,我本来就是只想做对样例0,1,4,测试一下自己的栈的使用来着。


2.某次提交结果

提交时间 状态 分数 题目 编译器 耗时 用户
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 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;
}

3.错误分析

实际上,所有测试样例都是有多位整数的情况的。这就很坑了,要么彻底对,要么一点不对。

那么,为什么会提示格式错误而不是答案错误呢?

这是因为假设输入是

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 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 / +

转换为前缀表达式同理。

----------------------------------------------------------------------------------------------------------------------------------------------------------

六、总结

其实这题的算法思想就跟栈没有什么关系,只是在解题过程中借用了一下栈这种数据结构来表达而已。大多数实际问题和算法题也都是这样的,本身解法与数据结构并无关系,只是在借助该类数据结构以一种简洁的方式来表示算法的思想,有没有这种类型的数据结构并不影响问题的解决,因为会在解决问题过程中自然而然的去发现和创造出这种数据结构(非常慢......)。不可以舍本逐末,本末倒置,只关心工具本身,要能明白问题的起源和终结问题的方法,这样才能掌握其本质。

----------------------------------------------------------------------------------------------------------------------------------------------------------

下一篇文章我们将使用创建二叉树+遍历二叉树的方法解决此题。

你可能感兴趣的:(数据结构与算法)