目录
一、概述
比较难的一个点,之前看CSAPP的时候里面有提到中缀表达式和后缀表达式。当时看的时候就没有明白,今天结合老师的代码仔细地顺一下这个思路。
二、代码
一些栈的基本操作就不再赘述,讲几个核心函数
2.1 访问栈顶函数
/*访问栈顶元素*/
int TopElem(CharStackPtr TempStack, char *e)
{
if (TempStack->top == -1)
{
return ERROR;
}
*e = TempStack->data[TempStack->top];
return OK;
}
弹栈操作会让Top值变化,有时候我们只是需要访问以下栈顶元素,方便后续的判断。
2.2 符号优先级判断
static char priority[128] = {0};
/*判断符号优先级*/
void Init_Priority()
{
/*
通过利用各个符号的ASCII码进行一个数组赋值
数值越低,优先级越高
*/
priority['+'] = 3;
priority['-'] = 3;
priority['*'] = 2;
priority['/'] = 2;
priority['('] = 1;
priority[')'] = 1;
}
在后续判断是否需要讲符号栈和数字栈的数字取出来进行计算,我们需要判断当前符号和栈顶符号优先级的先后顺序。
这里巧妙地地方在于,把整个数组设置成128长度,然后使用符号地ASCII码进行储存,浪费了很多地空间,但是一定程度上优化了可读性。
2.3 符号优先级判断
/*比较运算符号的优先级*/
int Compare_Priority(char op1, char op2)
{
/*如果op1优先于op2,返回正整数*/
return priority[op2] - priority[op1];
}
书接上文,对操作符优先级地判断。
2.4 操作符栈与数据栈的计算
/*运算符和操作数进行运算*/
int Op_Num_Col(CharStackPtr Nums, CharStackPtr Ops)
{
int a, b;
char op;
Pop(Ops, &op);
if (OK != Pop(Nums, &b))
{
return ERROR;
}
if (OK != Pop(Nums, &a))
{
return ERROR;
}
switch (op)
{
case '+':
{
Push(Nums, a + b);
break;
}
case '-':
{
Push(Nums, a - b);
break;
}
case '*':
{
Push(Nums, a * b);
break;
}
case '/':
{
Push(Nums, a / b);
break;
}
}
return OK;
}
需要获取操作符栈顶元素,以及操作数栈顶的两个元素,这里非常非常容易的一个错误就是ab获取的先后顺序,在除法和减法中他们的顺序安排。
2.5 核心计算
int Calculate(char *exp, int *result)
{
CharStackPtr Nums, Ops;
Nums = (CharStackPtr)malloc(sizeof(CharStack));
Nums->top = -1;
Ops = (CharStackPtr)malloc(sizeof(CharStack));
Ops->top = -1;
int flag_NumJudge = 0; // 1代表是数字,0代表非数字
int temp = 0;
int op;
while (*exp != 0)
{
/*如果是数字,关键判断是不是连续多位数*/
if (isdigit(*exp))
{
if (flag_NumJudge == 1)
{
Pop(Nums, &temp);
}
else
{
temp = 0;
}
flag_NumJudge = 1; //表面现在操作的符号是一个数字
temp = temp * 10 + *exp - '0'; //注意这个减去'0'
Push(Nums, temp);
}
/*如果是符号,关键看目前遇到的这一层符号优先级是不是低于栈里面的元素*/
else if ('/' == *exp || '*' == *exp || '+' == *exp || '-' == *exp)
{
flag_NumJudge = 0;
while ((Ops->top > -1) && //首先操作符栈要有数
(OK == TopElem(Ops, &op)) && //注意这里不是弹栈,是得到栈顶元素
'(' != op &&
')' != op && //这个操作符是加减乘除
(Compare_Priority(*exp, op) <= 0)) //在里面的操作符是优先于目前遇到的符号的
//比如 2*6+9 所以这一步可以去算2*9
//如果是2+6*9 这里的9应该是和下面的去匹配,所以不会去运算
{
Op_Num_Col(Nums, Ops);
}
Push(Ops, *exp);
}
/*左括号直接入栈*/
else if ('(' == *exp)
{
flag_NumJudge = 0;
Push(Ops, *exp);
}
/*右括号处理和左括号之间的所有符号数*/
else if (')' == *exp)
{
flag_NumJudge = 0;
while (OK == TopElem(Ops, &op) && '(' != op)
{
Op_Num_Col(Nums, Ops);
}
Pop(Ops, &op);
}
else
{
flag_NumJudge = 0;
}
exp++;
}
//结果这一层一定把括号都处理完了
while (Ops->top > -1 && Nums->data > -1)
{
Op_Num_Col(Ops, Nums);
}
return 0;
}
这个是核心的函数,我们一点点看其中关键的核心设计。该函数涉及多个循环,第一个循环,也就是最外层的循环是用于指针的移动,移动表达式数组的。增加了一个标志变量flag,用于配合一个while循环把数字全部统计成int类型,比如“698”这是一个符号串。这一步讲符号串化成198(int)
如果目前读到的元素是操作符,需要判断这个操作符与栈顶操作符的优先关系,如果是小于,可以进行计算,如果不是则NO。代码中增加的注释能更好地理解这一点。
如果是左括号,直接入栈。
如果是右括号,去寻找左括号,并在里面讲可以计算地进行计算
最后出来整个训话之后,能确保地一点是括号全部被操作,但是依然可能存在一些留存地操作符和操作数。需要最后再将栈去空。但此时其实是已经将表达式进行了后缀表达式地转换。
2.6 测试样例
2.7 测试结果
三、总结&反思
虽然代码出了正确结果,但有一些地方我觉得可能在边缘测试样例上存在问题。并不是完全自己独立写出来的,尤其是核心函数,那个顺序的安排,以及在第二个关键while中的截至条件,参考了CSDN和B站上的思路。
我觉得关键的难点是,对于表达式字符串指针的把握,因为不止在一个while循环中这个指针会改变。
理解这个代码的关键我觉得可以去去康康,中缀表达式和后缀表达式,在写的过程中,尤其是符号优先级的对于是否要计算的影响,不会写,本质是没明白怎么转成后缀表达式,在理解转表达式的基础上,一些地方也就解决了。