从零开始写一个数学表达式解析器
解析并计算数学表达式的方法有多种,下面介绍两种常见的方法:
1.中缀表达式转后缀表达式 + 后缀表达式求值:
将中缀表达式转换为后缀表达式
使用栈结构对后缀表达式进行求值。
2. 递归下降解析:
根据表达式的语法规则,用递归函数解析表达式。
遍历语法树,执行相应的计算操作得到最终结果。
一、逆波兰表达式(RPN)
逆波兰表达式(Reverse Polish Notation,RPN),也称为后缀表达式,是一种用于表示数学表达式的记法方式。在逆波兰表达式中,运算符位于操作数的后面,因此也被称为后缀表示法。
相比于常见的中缀表示法(如 3 + 4 * 2),逆波兰表达式具有以下特点:
没有括号:逆波兰表达式不需要括号来确定运算的顺序,因为它通过运算符的位置来明确操作数的顺序。
顺序明确:逆波兰表达式通过将运算符放在操作数的后面,可以明确指定操作的顺序。例如,逆波兰表达式 "3 4 2 * +" 表示的是 3 + (4 * 2)。
逆波兰表达式的计算过程通常使用栈来实现。算法遍历逆波兰表达式中的每个元素,遇到操作数时将其入栈,遇到运算符时从栈中弹出相应数量的操作数进行计算,并将计算结果重新入栈。最终,栈中剩下的元素就是计算结果。
以下是一个示例来说明逆波兰表达式的计算过程:
逆波兰表达式: 3 4 2 * +
从左到右遍历表达式的每个元素。
遇到数字 3,入栈。
遇到数字 4,入栈。
遇到数字 2,入栈。
遇到运算符 *,从栈中弹出操作数 2 和 4,计算 4 * 2 的结果 8,将结果入栈。
遇到运算符 +,从栈中弹出操作数 8 和 3,计算 8 + 3 的结果 11,将结果入栈。
遍历结束,栈中剩下的元素 11 就是最终的计算结果。
因此,逆波兰表达式 “3 4 2 * +” 的计算结果为 11。
二、将中缀表达式转化为逆波兰表达式
将中缀表达式转化为逆波兰表达式的常用方法是使用栈来处理。以下是一种常见的算法步骤:
创建一个空栈和一个空的输出队列(或列表)用于存储逆波兰表达式。
从左到右扫描中缀表达式的每个符号(操作数和运算符)。
如果遇到操作数(数字),将其添加到输出队列中。
如果遇到左括号 "(",将其压入栈中。
如果遇到右括号 ")",则弹出栈中的元素,并将弹出的元素添加到输出队列中,直到遇到左括号为止。注意:左右括号不添加到输出队列中。
如果遇到运算符,判断其与栈顶运算符的优先级:
- 如果栈为空,或栈顶为左括号 "(",则将运算符压入栈中。
- 否则,将栈顶运算符与当前运算符进行比较:
- 如果栈顶运算符的优先级大于或等于当前运算符,将栈顶运算符弹出并添加到输出队列中,直到栈顶运算符的优先级小于当前运算符,或栈为空,然后将当前运算符压入栈中。
重复步骤2至步骤6,直到扫描完所有符号。
将栈中剩余的运算符依次弹出并添加到输出队列中。
输出队列中的元素即为转化后的逆波兰表达式。
以下是一个简单的示例,演示了如何将中缀表达式转化为逆波兰表达式:
中缀表达式: 3 + 4 * 2 / (1 - 5)^2
转化为逆波兰表达式: 3 4 2 * 1 5 - 2 ^ / +
栈的实现
首先,我们定义一个结构体来表示栈:
#define MAX_SIZE 100
typedef struct {
int data[MAX_SIZE];
int top;
} Stack;
接下来,我们可以定义一些操作来操作栈的各个功能,比如初始化栈、判断栈是否为空、判断栈是否已满、入栈、出栈等:
void initialize(Stack *stack) {
stack->top = -1; // 初始化栈顶索引为-1,表示栈为空
}
int isEmpty(Stack *stack) {
return (stack->top == -1); // 栈为空时返回1,否则返回0
}
int isFull(Stack *stack) {
return (stack->top == MAX_SIZE - 1); // 栈满时返回1,否则返回0
}
void push(Stack *stack, int value) {
if (isFull(stack)) {
printf("Stack overflow\n"); // 栈已满,无法入栈
return;
}
stack->top++; // 栈顶索引加1
stack->data[stack->top] = value; // 将元素压入栈顶
}
int pop(Stack *stack) {
if (isEmpty(stack)) {
printf("Stack underflow\n"); // 栈为空,无法出栈
return -1; // 返回一个特殊值表示出错
}
int value = stack->data[stack->top]; // 取出栈顶元素的值
stack->top--; // 栈顶索引减1
return value;
}
解析算法实现
首先需要对字符串中的运算符进行识别与优先级判断,这一步通过isOperator和getPrecedence函数实现,代码如下:
int isOperator(char ch)
{
return (ch == '+' || ch == '-' || ch == '*' || ch == '/');
}
int getPrecedence(char ch)
{
if (ch == '+' || ch == '-')
return 1;
else if (ch == '*' || ch == '/')
return 2;
return 0;
}
接下来需要将中缀表达式转化为RPN,定义函数infixToPostfix:
void infixToPostfix(char *infix, char *postfix) //传入两个字符指针,分别对应中缀表达式与RPN
Stack stack;
initialize(&stack); //栈初始化
int i, j;
char ch;
for (i = 0, j = 0; infix[i] != '\0'; i++)
{
ch = infix[i];
if (isalnum(ch)) //判断是否为数字,这里的isanum函数需要先包含<ctype.h>才能使用
{
postfix[j++] = ch;//是数字,进入输出队列
}
else if (ch == '(') //判断是否是左括号
{
push(&stack, ch);//是左括号,压入栈顶
}
else if (ch == ')') //判断是否是右括号
{
while (!isEmpty(&stack) && stack.data[stack.top] != '(')
{
postfix[j++] = pop(&stack);//弹出栈中的元素,并将弹出的元素添加到输出队列中,直到遇到左括号为止,注意:左右括号不添加到输出队列中。
}
if (!isEmpty(&stack) && stack.data[stack.top] == '(')
{
pop(&stack);//弹出栈顶左括号但不添加到输出队列中。
}
else
{
printf("Invalid expression\n");//判断表达式是否合法
return;
}
}
else if (isOperator(ch)) //判断是否遇到运算符
{
while (!isEmpty(&stack) && stack.data[stack.top] != '(' &&getPrecedence(stack.data[stack.top]) >= getPrecedence(ch))
{
postfix[j++] = pop(&stack);//如果栈顶运算符的优先级大于或等于当前运算符,将栈顶运算符弹出并添加到输出队列中,直到栈顶运算符的优先级小于当前运算符
}
push(&stack, ch);//然后将当前运算符压入栈中。
}
}
while (!isEmpty(&stack) && stack.data[stack.top] != '(')
{
postfix[j++] = pop(&stack);//将剩余元素从栈中弹出,不包含括号
}
postfix[j] = '\0';
}
最后计算RPN的值,定义evaluatePostfix函数:
int evaluatePostfix(char *postfix) {
Stack stack;
initialize(&stack);
int i, operand1, operand2, result;
for (i = 0; postfix[i] != '\0'; i++) {
char ch = postfix[i];
if (isdigit(ch)) {
push(&stack, ch - '0');
} else if (isOperator(ch)) {
operand2 = pop(&stack);
operand1 = pop(&stack);
switch (ch) {
case '+':
result = operand1 + operand2;
break;
case '-':
result = operand1 - operand2;
break;
case '*':
result = operand1 * operand2;
break;
case '/':
result = operand1 / operand2;
break;
default:
printf("Invalid operator\n");
return 0;
}
push(&stack, result);
}
}
return pop(&stack);
}
main函数:
int main() {
char infix[MAX_SIZE];
char postfix[MAX_SIZE];
printf("Enter an infix expression: ");
fgets(infix, sizeof(infix), stdin);
infixToPostfix(infix, postfix);
printf("Postfix expression: %s\n", postfix);
int result = evaluatePostfix(postfix);
printf("Result: %d\n", result);
return 0;
}
添加对浮点数的支持
以上程序基于整数运算,不支持浮点数,在进行整数除法时结果将丢弃小数部分
为了支持浮点数,需要对字符串中的小数进行识别,首先定义getUserInput()接收用户写入的表达式:
char* getUserInput()
{ #define MAX_SIZE 2000
static char input[MAX_SIZE];
printf("输入表达式:");
fgets(input, MAX_SIZE, stdin);
return input;
}
定义isDecimal(),用于判断一个字符串是否为小数
bool isDecimal(const char* str)
{
char* endptr;
strtod(str, &endptr);
// 检查转换后的字符串是否为空,且遍历完所有字符
if (*endptr == '\0' && endptr != str) {
return true;
}
return false;
}
接下来对用户输入的表达式进行处理,注意到此时表达式中既有double型数据(小数)也有char型数据(括号和运算符),我们需要更该数据的储存方式以实现在一个数组中同时存放char和double型数据。这里我们用结构体数组的方式来实现
typedef struct
{
double doubleValue;//存放小数的值
char charValue;//存放符号
bool isNum;//判断当前结构体存放的数据类型
} DataItem;
DataItem infix[MAX_SIZE];//存放中缀表达式的结构体数组
定义input2infix函数将输入的字符串转换为中缀表达式
DataItem* input2infix(char *input)
{
char temp[MAX_SIZE] = ""; // 临时字符串,用于构建数字
DataItem infix[MAX_SIZE];
DataItem Postfix[MAX_SIZE];
double number;
int i = 0;
int index = 0;
while (input[i] != '\0') {
if (isdigit(input[i]) || input[i] == '.') {
// 将数字字符或小数点添加到临时字符串中
strncat(temp, &input[i], 1);
}
else
{
// 当遇到非数字字符时,检查临时字符串是否为小数
if (isDecimal(temp))
{
number = atof(temp);
infix[index].doubleValue = number;
infix[index].isNum = true;
index++;
}
// 重置临时字符串
temp[0] = '\0';
if (input[i]=='(')
{
infix[index].charValue = input[i];
infix[index].isNum = false;
index++;
}
if (input[i] == ')')
{
infix[index].charValue = input[i];
infix[index].isNum = false;
index++;
}
if (isOperator(input[i]))
{
switch (input[i]) {
case '+':
infix[index].charValue = input[i];
infix[index].isNum = false;
index++;
break;
case '-':
infix[index].charValue = input[i];
infix[index].isNum = false;
index++;
break;
case '*':
infix[index].charValue = input[i];
infix[index].isNum = false;
index++;
break;
case '/':
infix[index].charValue = input[i];
infix[index].isNum = false;
index++;
break;
default:
//printf("Invalid operator\n");
return 0;
}
}
}
i++;
}
infix[index].charValue = '\0';
infix[index].isNum = false;
return infix;
}
在主函数中调用 DataItem* infix= input2infix(getUserInput());
就可以将输入的表达式转换为含浮点数中缀表达式了
然后照猫画虎,把中缀表达式转化为RPN。别忘了我们更改了数据的储存方式,所以对于栈的实现与操作也需要做相应的修改:
typedef struct
{
DataItem data[MAX_SIZE];//这里将data类型改为前面定义的DataItem结构体
int top;
} Stack;
//由于压入栈中的数据有char和double类型,所以要有两种对应的压栈操作
void pushChar(Stack* stack, char value)
{
if (isFull(stack)) {
printf("Stack overflow\n");
return;
}
stack->top++;
stack->data[stack->top].charValue = value;
stack->data[stack->top].isNum = false;
}
void pushDouble(Stack* stack, double value)
{
if (isFull(stack)) {
printf("Stack overflow\n");
return;
}
stack->top++;
stack->data[stack->top].doubleValue = value;
stack->data[stack->top].isNum = true;
}
double popDouble(Stack* stack) {
if (isEmpty(stack)) {
printf("Stack underflow\n");
return '\0';
}
double value = stack->data[stack->top].doubleValue;
stack->top--;
return value;
}
char popChar(Stack* stack) {
if (isEmpty(stack)) {
printf("Stack underflow\n");
return '\0';
}
char value = stack->data[stack->top].charValue;
stack->top--;
return value;
}
最后修改infixToPostfix函数和evaluatePostfix函数就大功告成了,修改比较简单,注意更改数据类型并增添对char和double类型的判断逻辑即可,这里我直接贴代码了。
void infixToPostfix(DataItem* infix, DataItem* postfix)
{
Stack stack;
initialize(&stack);
int i, j;
for (i = 0, j = 0; !((infix[i].isNum == false) && (infix[i].charValue == '\0')); i++)
{
if (infix[i].isNum == true)//遇到数字,添加到输出队列
{
postfix[j].doubleValue = infix[i].doubleValue;
postfix[j].isNum = infix[i].isNum;
j++;
}
else if ((infix[i].isNum == false) && (infix[i].charValue == '(')) //压入栈中
{
pushChar(&stack, infix[i].charValue);
}
else if ((infix[i].isNum == false) && (infix[i].charValue == ')'))
{
while ((!isEmpty(&stack)) && (!((stack.data[stack.top].isNum == false) && (stack.data[stack.top].charValue== '('))))
{
if (infix[i].isNum)
{
postfix[j].doubleValue = popDouble(&stack);
postfix[j].isNum = infix[i].isNum;
j++;
}
else
{
postfix[j].charValue = popChar(&stack);
postfix[j].isNum = false;
j++;
}
}
if ((!isEmpty(&stack)) && ((stack.data[stack.top].isNum == false) && (stack.data[stack.top].charValue == '(')))
{
popChar(&stack);
}
else {
printf("Invalid expression\n");
return;
}
}
else if ((infix[i].isNum == false) && (isOperator(infix[i].charValue)))
{
while (!isEmpty(&stack) && stack.data[stack.top].charValue != '(' &&
getPrecedence(stack.data[stack.top].charValue) >= getPrecedence(infix[i].charValue))
{
postfix[j].charValue = popChar(&stack);
postfix[j].isNum = false;
j++;
}
pushChar(&stack, infix[i].charValue);
}
}
while (!isEmpty(&stack) && stack.data[stack.top].charValue != '(') {
postfix[j].charValue = popChar(&stack);
postfix[j].isNum = false;
j++;
}
postfix[j].charValue = '\0';
postfix[j].isNum = false;
}
main函数
int main()
{
double reslut;
DataItem Postfix[MAX_SIZE];
DataItem* infix= input2infix(getUserInput());
printf("Infix:");
for (int k = 0; !((infix[k].isNum==false)&&(infix[k].charValue=='\0')); k++)
{
if (infix[k].isNum)
printf("%.2f", infix[k].doubleValue);
else
{
printf("%c", infix[k].charValue);
}
}
printf("\n");
infixToPostfix(infix, Postfix);
reslut = evaluatePostfix(Postfix);
printf("The reslut: %.2f", reslut);
return 0;
}
测试用例
((3 + 4) * (5 - 2) + 7) / (8 * (6 - 1))
((2.5 + 3.2) * (4.7 - 1.8)) / ((5.3 + 2.1) - (6.4 * 0.5)) + (9.6 - 10.3) * 11.2