前言
AI练中学中有一道《简单四则运算解析器》,思路不难,coding很苦手。最近有个学妹过来问了一道一模一样的,还展示了其长达200行的糨糊代码。不仅很挑战人类的思维内存,还猛烈攻击了我脆弱的双眼。
但是思路没什么问题,甚至和我想得一样,栈套栈。于是我开始思考能不能用递归来解决括号的问题,这样逻辑就能分离成独立的括号递归和独立的不带括号的四则运算。
尝试了一下,发现也不是很难,而且这题也算得上是入门基础(期末考试常考),能搞个简单点的版本也能给迫在眉睫的期末考试降降温。(小白请注意,一般这道题都是老师拿来练练你们coding能力的,最好还是硬解。但是多个思路也能让你期末软着陆也不错)
事不宜迟,让我们马上开始!
思路
首先摆出题面::::tips
小F面临一个编程挑战:实现一个基本的计算器来计算简单的字符串表达式的值。该字符串表达式有效,并可能包含数字(0-9)、运算符+
、-
及括号()
。注意,字符串中不包含空格。除法运算应只保留整数结果。请实现一个解析器计算这些表达式的值,且不使用任何内置的eval
函数。
测试样例
_**样例1:**_输入:expression = "1+1"
输出:2
样例2:
输入:expression = "3+4*5/(3+2)"
输出:7
样例3:
输入:expression = "4+2*5-2/1"
输出:12
样例4:
输入:expression = "(1+(4+5+2)-3)+(6+8)"
输出:23
样例5:
输入:expression = "2*(5+5*2)/3+(6+8*3)"
输出:40
:::
简单说明一下:
**1.input:**一串字符串,含有数字、运算符以及括号,请注意,题目中虽然没有说有乘法符号和除法符号,但是下面的样例中全都是,所以也没得偷懒。
2.处理步骤:
1.从字符串中提取出数字、运算符以及括号。
2.括号的递归逻辑。
3.不带括号的入栈出栈逻辑。
**3.output:**一个表示结果的整型数字。
具体步骤
ps:请注意:虽然我写的是第一步、第二步、第三步。但是它们之间并不是严格的顺序执行关系。遍历字符串的行为就注定这个程序段不能是顺序执行。可以想想我们人工处理四则运算表达式时也不能做到顺序遍历字符串的过程中顺序处理,计算机在模仿我们人工处理的过程,自然也不是顺序执行语句。
同时我这三步中还暗含了计算逻辑,因为计算逻辑是无法从括号的递归逻辑和不带括号的入栈出栈逻辑中单独分离出来的。
第一步:从字符串中提取出数字、运算符以及括号。
:::tips 这一步没什么难度。一般没人卡在这里,我就简单过一下。
首先遍历字符串,逐位判断,如果是数字就先存在整型变量里
下一个字符如果还是数字就继续存在刚刚那个整型变量里(var*10+char-'0',因为是字符,别忘了-'0');
下一个字符如果是运算符就将整型变量入栈;
下一个字符如果是括号就递归(也分左右括号,这里就简单调一下,下面细说);
:::
第二步:不带括号的入栈出栈逻辑。
:::tips 通过上述逻辑,我们可以将问题简化成含有整型变量,运算符的一个四则运算表达式了。
首先开辟两个栈区,一个存放整型变量,一个存放运算符。
在遍历这个四则运算表达式的过程中,我们会遇到两种情况:
整型变量:进入整型变量栈区
运算符:进入运算符栈区,同时我们也需要关注栈区中的运算符:
如果要进入栈区的运算符是“+ - * /” :
请注意这里是不带括号的入栈出栈逻辑,你不必考虑左右括号。
那么你可以认为所有括号内的表达式都已完成,现在仅剩下加减乘除四则运算了。
我不想去思考什么优先级大压小,小压大的问题,那太不优雅了。
直接运用我们小学一年级的解法就是:先乘除后加减。
那我们不妨就定义直到四则运算表达式的末尾,我们才开始加减运算。在此之前遇到乘法我们就乘,遇到除法我们就除。
:::
:::tips ps:刚开始写代码的时候,我被栈给限制住了,想着最后一个符号是“”或“/”那就会让一个“”或“/”留下来
结果发现写道后面这个问题很自然地就被解决了,因为你会自然地补上“+”和“-”的逻辑,好久没体验过这么奇妙的感觉了。就像是学书法时,“泄意未尽,以点补之”的那个“神”字(赵孟頫的千字文)不用动脑的感觉真好(bushi.....
:::
:::tips 如果要进入栈区的运算符是“(”:由于括号内的表达式优先级更高,不妨调用solution函数本身,即递归调用,这样就能实现括号内的表达式优先计算。
如果要进入栈区的运算符是“)”:右括号代表着当前层级的调用结束,我们需要返回上一层函数调用,这就意味着我们的solution函数的返回条件应设为检测到右括号。返回值即当前层级的(当前括号内的)表达式的值。
:::
第三步:括号的递归逻辑。
:::tips 递归逻辑比较简单,但是有两点需要注意:
返回值:
当前表达式(括号内部)的值,根据我们上述的分析,你需要调用的是先乘除再加减的逻辑,这一步尤其关键,你可以将乘除的步骤放在入栈函数中,这样的好处是你可以不必考虑最后一个操作符的问题,也就是我上述的ps部分。
当然你也可以选择加减乘除逻辑一体,这样的好处就是函数调用次数少,上下文切换频次少,速度更快,但是你就需要小心自己的边界问题了(学妹的代码真是没眼看,一直段错误,给我干懵了)。
原字符串指针:
这是一个很重要的传递值,因为我们是遍历字符串得到的O(n)算法,所以你需要保存当前字符串的位置,不能递归着递归着就把位置信息丢了。为了简化逻辑,我选择的是用全局变量来表示指针(即当前字符串的下标,不是狭义上的指针)。
:::
下面是AC截图,各位放心使用
代码如下:
#include <iostream>
#include <vector>
#include <string>
using namespace std;
int where; // 全局变量,用于记录当前位置
// 辅助函数:处理当前的操作数和运算符
void push(vector<int> &numbers, vector<char> &ops, int cur, char op) {
int n = numbers.size();
if (n == 0 || ops.back() == '+' || ops.back() == '-') {
numbers.push_back(cur);
ops.push_back(op);
} else {
int topNumber = numbers.back();
char topOp = ops.back();
numbers.pop_back();
ops.pop_back();
if (topOp == '*') {
numbers.push_back(topNumber * cur);
} else {
numbers.push_back(topNumber / cur);
}
ops.push_back(op);
}
}
// 辅助函数:计算最终结果
int compute(const vector<int> &numbers, const vector<char> &ops) {
int n = numbers.size();
int ans = numbers[0];
for (int i = 1; i < n; i++) {
ans += (ops[i - 1] == '+' ? numbers[i] : -numbers[i]);
}
return ans;
}
// 主函数:递归解析表达式
int f(const string &s, int i) {
int cur = 0;
vector<int> numbers;
vector<char> ops;
while (i < s.size() && s[i] != ')') {
if (isdigit(s[i])) {
cur = cur * 10 + (s[i++] - '0');
} else if (s[i] != '(') {
// 遇到运算符 + - * /
push(numbers, ops, cur, s[i++]);
cur = 0;
} else {
// 遇到左括号,递归计算
cur = f(s, i + 1);
i = where + 1; // 更新全局位置
}
}
push(numbers, ops, cur, '+'); // 最后处理当前数字
where = i; // 更新全局位置
return compute(numbers, ops);
}
int solution(const string &expression) {
where = 0;
return f(expression, 0);
}