在开始分析之前,首先要感谢我的一位同级不同院的朋友,是他构造了代码框架,提供了绝大部分代码,并向我介绍了后缀表达式,我仅仅进行了代码的整理以及功能函数化的工作。没有他的帮助,我绝不可能这么快就解决这道题,也不会了解到后缀表达式这一工具。
这题要求我们计算具有优先级、含有整数与小数的四则运算表达式。在之前我们曾做过无优先级、仅含整数的四则运算题目,与之相比难度不在同一个层次。
这题的最大难点在于如何按照优先级进行计算。我最开始希望利用tokenizer将表达式进行切分,随后从最内层括号开始依次进行运算,但繁杂的工作量让我望而却步。我的那位同级朋友向我介绍了后缀表达式的概念,后缀表达式是一种无需括号等工具即可表现多层优先级的运算表达方式,相比起中缀表达式——即我们日常使用的表达式,它可以更方便地用计算机计算。详细的介绍可以参考以下文章:《数据结构》:中缀表达式转后缀表达式 + 后缀表达式的计算-CSDN博客
要利用后缀表达式,我们必须先将中缀表达式转换至后缀表达式,随后按照运算规则进行计算。
表达式的转换需要用到栈这一数据结构,方便起见,我们在这里用线性表来模拟栈,并将变量全部设置为全局变量,代码框架如下:
void clear();//初始化变量
void convert_to_RPN();//将中缀表达式转换为后缀表达式
void operate();//计算后缀表达式
char str[500], rpn[500];
int main() {
while (1) {
clear();//初始化变量
scanf("%s", str);
if (str[0] == '=') break;
convert_to_RPN();//将中缀表达式转换为后缀表达式
operate();//计算后缀表达式
printf("%.1f\n", num[0]);
}
return 0;
}
clear函数用于初始化各种变量,具体不再多写。
conver_to_RPN函数用来转换表达式,先来思考如何实现这一函数。
我们读取到的字符有以下几种情况:'+', '-', '*', '/', '(', ')', "0123456789", '.', '=', 我们将除数字、小数点与等号外的字符归为运算符。
首先处理读取到运算符的情况。首先我们要判断 '+', '-' 何时是加减号,何时是正负号。观察题目给出的例子与我们的经验,如果 '+', '-' 前面是数字或者是右括号,那么它就是加减号而不是正负号,此时它应该和运算符一样处理,其他情况下它是正负号,应该和数字一样处理。
对于运算符,我们准备了一个栈来存储它们。每当读取到运算符,如果这个时候栈是空的或者下面的运算符优先级较低,那么将这个运算符压入栈中;如果下面的运算符优先级相同或较高,那么将下面的运算符弹出到后缀表达式中,直到下面的运算符优先级较低为止。
如果读取到左括号,将其压入栈中,并且定义其为最低优先级运算符。如果读取到右括号,那么将左右括号之间的运算符依次弹出。注意,后缀表达式不含括号。
对于数字,我们将其直接放入表达式中。
如果读取完后栈中还有运算符剩余,那么将其依次弹出至表达式。
至此表达式已经转换完成,函数代码如下:
char str[500], stack_1[500], rpn[500];
//str存储中缀表达式,stack_1存储中缀转前缀过程中的运算符,rpn存储转换后的后缀表达式
int top_1 = 0, index_1 = 0;
//top_1为stack_1的栈顶索引,index_1为转换时rpn的索引
void pop() {//将栈顶元素弹出至后缀表达式中
rpn[index_1++] = stack_1[--top_1];
rpn[index_1++] = ' ';
}
void convert_to_RPN() {//将中缀表达式转换至后缀表达式
for (int i = 0; i < strlen(str); i++) {
if (str[i] == '-'||str[i] == '+')//如果是+或-,判断是否为正负号
if (i == 0 || !(str[i - 1] >= '0' && str[i - 1] <= '9'))
//当+或-为首位元素或前一位元素不是数字
if (str[i - 1] != ')')//并且前一位元素不是后括号
rpn[index_1++] = str[i++];//将正负号加入后缀表达式
while ((str[i] >= '0' && str[i] <= '9') || str[i] == '.') {//如果是数字
rpn[index_1++] = str[i++];//加入后缀表达式
if (!(str[i] >= '0' && str[i] <= '9') && str[i] != '.') {
//并在读取结束后添加空格
rpn[index_1++] = ' ';
}
}
if (str[i] == '(') {//如果是左括号,将其压入栈中
stack_1[top_1++] = str[i];
}
if (str[i] == ')') {
//如果是右括号,将在其与前一个左括号之间的运算符依次弹出至表达式
while (stack_1[top_1 - 1] != '(') pop();
top_1--;
}
if (str[i] == '+' || str[i] == '-') {//如果是加减号
while (top_1 && stack_1[top_1 - 1] != '(') {//假如栈内前一个元素不是更低级运算符
pop();//弹出所有同级或更高级的运算符
}
stack_1[top_1++] = str[i];//弹出结束后压入栈中
}
if (str[i] == '*' || str[i] == '/') {//如果是乘除号
while (top_1 && stack_1[top_1 - 1] != '(' && stack_1[top_1 - 1] != '-' && stack_1[top_1 - 1] != '+') {//如果栈内前一个运算符不是更低级的运算符
pop();//弹出所有同级或更高级的运算符
}
stack_1[top_1++] = str[i];//弹出结束后压入栈中
}
}
while (top_1) {//在读取完中缀表达式后如果栈中还有运算符
if (stack_1[top_1 - 1] != '(') {//依次弹出至表达式
pop();
}
}
}
operate用于计算后缀表达式,接下来考虑如何实现operate函数。
对于一个完整的后缀表达式,每次对第一个出现的运算符及这个运算符的前两个数字进行运算,并将运算完的结果留在原位,最后留下的数字即为最终结果。
我们可以准备一个栈来存储每个数的各位数字。每当读取到数字,就将其整个存入栈中,最后利用atof函数将其转换为浮点数存入num中。atof是声明于stdlib.h的函数,可以将字符串转换为double类型的变量。
遍历表达式,当读到数字,存储进num;当遇到运算符时,对num的最后两个元素进行计算,并将结果放在较靠前的那个元素位置中。如此循环,直到遍历完表达式,此时num[0]即为最后结果。
代码如下:
char rpn[500], stack_2[500];
//rpn存储转换后的后缀表达式,stack_2存储后缀表达式中的单个数字
double num[500] = { 0 };//num存储后缀表达式中的待运算数字及最终结果
int top_2 = 0, n = 0, index_2 = 0;
//top_2为stack_2的栈顶索引,index_2为计算时rpn的索引
void record_num() {//存储数字
stack_2[top_2++] = rpn[index_2++];
stack_2[top_2] = '\0';
}
void operate_add() {//加法运算
if (rpn[index_2] == '+') {
if (rpn[index_2 + 1] >= '0' && rpn[index_2 + 1] <= '9') record_num();
//如果是正负号 将其存入stack_2
else {//如果是加减号,进行运算
num[n - 2] = num[n - 2] + num[n - 1];
n--;
index_2++;
}
}
}
void operate_sub() {//减法运算
if (rpn[index_2] == '-') {
if (rpn[index_2 + 1] >= '0' && rpn[index_2 + 1] <= '9') record_num();
else {
num[n - 2] = num[n - 2] - num[n - 1];
n--;
index_2++;
}
}
}
void operate_mult() {//乘法运算
if (rpn[index_2] == '*') {
num[n - 2] = num[n - 2] * num[n - 1];
n--;
index_2++;
}
}
void operate_div() {//除法运算
if (rpn[index_2] == '/') {
num[n - 2] = num[n - 2] / num[n - 1];
n--;
index_2++;
}
}
void string_to_float() {//数字转换
while ((rpn[index_2] >= '0' && rpn[index_2] <= '9') || rpn[index_2] == '.') {
//如果是数字或小数点
record_num();//存储进stack_2
if (rpn[index_2] == ' ') {//读取完成后转换为浮点数,存入num
num[n++] = atof(stack_2);
top_2 = 0;
index_2++;
}
}
}
void operate() {//计算转换完成的后缀表达式
while (rpn[index_2]) {
operate_sub();//如果是加号,进行加法运算
operate_add();//如果是减号,进行减法运算
operate_mult();//如果是乘号,进行乘法运算
operate_div();//如果是除号,进行除法运算
if (rpn[index_2] == ' ') index_2++;//进行一次运算后跳过空格
string_to_float();//如果是数字,将其存储进stack_2,转化为浮点数后存入num
}
}
至此我们已经解决了绝大多数问题,只要注意索引的边界条件等细枝末节即可。需要提醒的是,如果使用fgets函数来进行读取,需要设法去除其末尾的换行符,否则会导致错误。
基于以上思路,完整代码如下:
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
char str[500] = { "" }, stack_1[500] = { "" }, rpn[500] = { "" }, stack_2[500] = { "" };
//str存储中缀表达式,stack_1存储中缀转前缀过程中的运算符,rpn存储转换后的后缀表达式,stack_2存储后缀表达式中的单个数字
double num[500] = { 0 };//num存储后缀表达式中的待运算数字及最终结果
int top_1 = 0, top_2 = 0, index_1 = 0, n = 0, index_2 = 0;
//top_1为stack_1的栈顶索引,top_2为stack_2的栈顶索引,index_1为转换时rpn的索引,index_2为计算时rpn的索引
void clear() {//每次读取数据前清空各个全局变量
top_1 = top_2 = index_1 = n = index_2 = 0;
memset(str, '\0', sizeof(str));
memset(stack_1, '\0', sizeof(stack_1));
memset(rpn, '\0', sizeof(rpn));
memset(stack_2, '\0', sizeof(stack_2));
memset(num, 0, sizeof(num));
}
void pop() {//将栈顶元素弹出至后缀表达式中
rpn[index_1++] = stack_1[--top_1];
rpn[index_1++] = ' ';
}
void convert_to_RPN() {//将中缀表达式转换至后缀表达式
for (int i = 0; i < strlen(str); i++) {
if (str[i] == '-'||str[i] == '+')//如果是+或-,判断是否为正负号
if (i == 0 || !(str[i - 1] >= '0' && str[i - 1] <= '9'))
//当+或-为首位元素或前一位元素不是数字
if (str[i - 1] != ')')//并且前一位元素不是后括号
rpn[index_1++] = str[i++];//将正负号加入后缀表达式
while ((str[i] >= '0' && str[i] <= '9') || str[i] == '.') {//如果是数字
rpn[index_1++] = str[i++];//加入后缀表达式
if (!(str[i] >= '0' && str[i] <= '9') && str[i] != '.') {
//并在读取结束后添加空格
rpn[index_1++] = ' ';
}
}
if (str[i] == '(') {//如果是左括号,将其压入栈中
stack_1[top_1++] = str[i];
}
if (str[i] == ')') {
//如果是右括号,将在其与前一个左括号之间的运算符依次弹出至表达式
while (stack_1[top_1 - 1] != '(') pop();
top_1--;
}
if (str[i] == '+' || str[i] == '-') {//如果是加减号
while (top_1 && stack_1[top_1 - 1] != '(') {//假如栈内前一个元素不是更低级运算符
pop();//弹出所有同级或更高级的运算符
}
stack_1[top_1++] = str[i];//弹出结束后压入栈中
}
if (str[i] == '*' || str[i] == '/') {//如果是乘除号
while (top_1 && stack_1[top_1 - 1] != '(' && stack_1[top_1 - 1] != '-' && stack_1[top_1 - 1] != '+') {//如果栈内前一个运算符不是更低级的运算符
pop();//弹出所有同级或更高级的运算符
}
stack_1[top_1++] = str[i];//弹出结束后压入栈中
}
}
while (top_1) {//在读取完中缀表达式后如果栈中还有运算符
if (stack_1[top_1 - 1] != '(') {//依次弹出至表达式
pop();
}
}
}
void record_num() {//存储数字
stack_2[top_2++] = rpn[index_2++];
stack_2[top_2] = '\0';
}
void operate_add() {//加法运算
if (rpn[index_2] == '+') {
if (rpn[index_2 + 1] >= '0' && rpn[index_2 + 1] <= '9') record_num();
//如果是正负号 将其存入stack_2
else {//如果是加减号,进行运算
num[n - 2] = num[n - 2] + num[n - 1];
n--;
index_2++;
}
}
}
void operate_sub() {//减法运算
if (rpn[index_2] == '-') {
if (rpn[index_2 + 1] >= '0' && rpn[index_2 + 1] <= '9') record_num();
else {
num[n - 2] = num[n - 2] - num[n - 1];
n--;
index_2++;
}
}
}
void operate_mult() {//乘法运算
if (rpn[index_2] == '*') {
num[n - 2] = num[n - 2] * num[n - 1];
n--;
index_2++;
}
}
void operate_div() {//除法运算
if (rpn[index_2] == '/') {
num[n - 2] = num[n - 2] / num[n - 1];
n--;
index_2++;
}
}
void string_to_float() {//数字转换
while ((rpn[index_2] >= '0' && rpn[index_2] <= '9') || rpn[index_2] == '.') {
//如果是数字或小数点
record_num();//存储进stack_2
if (rpn[index_2] == ' ') {//读取完成后转换为浮点数,存入num
num[n++] = atof(stack_2);
top_2 = 0;
index_2++;
}
}
}
void operate() {//计算转换完成的后缀表达式
while (rpn[index_2]) {
operate_sub();//如果是加号,进行加法运算
operate_add();//如果是减号,进行减法运算
operate_mult();//如果是乘号,进行乘法运算
operate_div();//如果是除号,进行除法运算
if (rpn[index_2] == ' ') index_2++;//进行一次运算后跳过空格
string_to_float();//如果是数字,将其存储进stack_2,转化为浮点数后存入num
}
}
int main() {
while (1) {
clear();//初始化全局变量
scanf("%s", str);
if (str[0] == '=') break;
convert_to_RPN();//将中缀表达式转换为后缀表达式
operate();//计算后缀表达式
printf("%.1f\n", num[0]);
}
return 0;
}
这道题利用后缀表达式解决确实更加清晰、方便。
解决这道题的过程中我了解到了许多字符串与数互相转换的函数,积累了用线性表模拟栈的实现的经验。
再次感谢我的那位朋友。