1. 先把题目扒干净:这道题在考什么
表达式求值这四个字听起来平平无奇,但把范围收窄到"只有加法和乘法、没有括号、数字最大到 2^31-1、结果只取后四位"之后,这道题的考察点就变得非常明确了——它不是考你会不会写一个通用解析器,而是考你能不能识别出运算符优先级的结构性,以及有没有意识到取模运算在什么条件下可以提前做。我第一次拿到这道题的时候,第一反应是上双栈,写完一百多行,看了别人的题解才发现自己被"表达式求值"这四个字带偏了:题目给的限制条件太强了,强到足以把问题降维成一次线性扫描。
先说清楚这道题的基本面。输入只有一行字符,字符集被严格限定在0-9、+、*这十二种,没有空格、没有括号、没有减号除法号,参与运算的数字都是 0 到 2^31-1 之间的整数,字符串长度上限十万。输出要求是:如果答案超过四位,只输出最后四位,且前导零不输出。也就是说,结果本质上是对 10000 取模,但你不能自作聪明地把4打印成0004,这一点坑过不少人。
题目给的样例很有信息量,值得逐个拆开看。第一个样例1+1*3+4输出8,这个例子专门用来验证你是不是真的处理了乘法优先级——如果从左往右无脑算,你会得到((1+1)*3)+4 = 10,错得很明显。第二个样例1+1234567890*1输出7891,这个例子在考"数字本身就要取模":1234567890 对 10000 取模是 7890,加一得 7891。第三个样例1+1000000003*1输出4,同理,1000000003 取模是 3,加一得 4。后两个样例的用意非常直白:它们在提醒你,取模这件事不是等到最后一步才做,而是要贯穿整个过程。
提示:这三个样例其实是一套完整的引导。第一题引导你处理优先级,第二、三题引导你处理溢出。很多人只看第一个样例就开始写,写完发现在大数据点上错得莫名其妙,就是漏掉了后面两个样例想告诉你的信息。
适合看这篇内容的人群,我觉得可以分三类。第一类是刚开始刷题的初学者,正在啃栈和字符串处理,这类题目是最合适的练手材料,因为它足够简单但又不至于毫无思维含量。第二类是有一定基础、但每次遇到"取模"就心里发虚的人,这道题可以帮你把"什么时候能取模、什么时候不能"这个念头彻底想清楚。第三类是想把表达式求值当成一个通用工具沉淀下来的人,这道题的简化版本可以作为一个很好的起点,往后加括号、加减法、加函数调用,一路能扩展到能写进项目里的公式引擎。三种人读同一篇文章,重点会不一样,我会尽量把这三种视角都覆盖到。
2. 思路拆解:为什么"分段求积再求和"是正解
2.1 运算优先级的本质是"表达式可以被切块"
想通这道题的关键,是把"优先级"这个词翻译成一个具体的结构。加法和乘法混在一起,乘法的结合力更强,所以一个只有+和*的表达式,天然可以被+切成若干段,每一段内部只有乘法,段与段之间只有加法。比如a+b*c*d+e*f,切开就是[a]、[b*c*d]、[e*f]三段,每段算出一个乘积,最后把所有乘积加起来。这个观察一旦成立,整个问题就变成了"顺序扫描字符串、维护一个当前乘积、遇到加号就把乘积结算掉"的线性流程,栈都不需要。
我更愿意把这种解法叫做"延迟结算"。你手上永远有两个累加器:一个存已经确定下来的和,一个存当前还在长的乘积。读到数字就往乘积里乘,读到乘号什么也不做(因为后面还会继续乘),读到加号就把当前乘积结算进和里,然后把乘积重置回 1。这个模型的好处是它完全符合人的直觉——你在纸上算2*3+4*5的时候,也是先算 6 和 20,再相加,没有人会真的先去算3+4。
对比一下双栈方案,你就能看出线性扫描的优势在哪里。双栈(一个数字栈、一个符号栈)是通用表达式求值的标准做法,它的正确性毋庸置疑,扩展到带括号、带减法除法、带一元负号都没问题。但它的代价是:需要额外的空间,需要维护运算符优先级表,需要在读到末尾时把栈清空,代码量至少翻三倍,出错点也成倍增加。在这道题的限制条件下,双栈属于典型的"用牛刀杀鸡"。
| 方案 | 时间复杂度 | 空间复杂度 | 代码量 | 扩展性 | 本题适用性 |
|---|---|---|---|---|---|
| 线性扫描 + 延迟结算 | O(n) | O(1) | 约 20 行 | 差,仅限无括号加减乘 | 最优 |
| 双栈 + 优先级表 | O(n) | O(n) | 约 80 行 | 好,可扩展括号与四则 | 能用但浪费 |
| 递归下降 | O(n) | O(递归深度) | 约 60 行 | 很好,语法结构清晰 | 能用但浪费 |
按+切分 + 内部按*切分 | O(n) | O(n) | 约 8 行 | 差 | 适合验证,不适合正式提交 |
我个人的习惯是:比赛里遇到这种限制明确的题,先写最快的线性扫描;如果时间充裕,再补一个切分版本用来对拍。两个独立实现的答案一致,心里才踏实。
2.2 边算边取模到底安不安全
这是这道题最容易让人犹豫的地方。很多人心里有个模糊的印象:"取模会丢信息,不能随便提前做。"这个印象在减法、除法、比较大小这些场景下是对的,但在本题里完全不成立。原因很简单:加法和乘法对取模是同态的。
用式子写出来就是(a + b) mod m = ((a mod m) + (b mod m)) mod m,以及(a * b) mod m = ((a mod m) * (b mod m)) mod m。这两条性质不是近似,是恒等。所以你在任意时刻把任意参与运算的数替换成它模 10000 的余数,最终结果模 10000 的余数都不会变。本题只涉及加法和乘法,没有任何一条运算会破坏这个性质,取模可以放心地、随时随地地做。
反过来说,如果不提前取模会发生什么?表达式长度十万,假设全是2*2*2*...,那结果就是 2 的十万次方,这个数有大约三万个十进制位。C++ 里没有任何内置类型能装下,long long在第 63 次乘二的时候就溢出了。所以不是因为"取模更好",而是因为"不取模根本算不出来"。这一点必须想明白,否则你会觉得取模是一种偷懒的技巧,而不是一种必要的正确性保障。
那数字本身需不需要取模?严格来说,题目给的单个数最大 2147483647,只有十位,用long long存绰绰有余,不取模也不会溢出。但我在实现里还是习惯性地给数字累加的那一步加上取模,理由是:一旦养成这个习惯,遇到数字位数更长的变种题(比如一千位的整数)时不用改代码;而且加上取模之后,参与乘法的两个因子都严格小于 10000,乘积小于 10^8,连int都不会溢出,内存和寄存器的压力都小。这点微小的一致性收益,我觉得值。
注意:这套"随便取模"的结论只在不含减法和除法的前提下成立。一旦题目加入减号,中间结果取模后可能出现负数,必须用
((x % m) + m) % m修正;一旦加入除法,取模环境下的除法需要求逆元,只有在模数为质数且除数不被整除时才好办。所以这道题的经验不能无脑迁移到四则运算版本,这是两回事。
2.3 那些看起来聪明其实会翻车的写法
我先说说最容易踩的一个坑:直接eval。由于输入字符串恰好是一个合法的 Python 表达式,很多人会想,一行print(eval(input()) % 10000)不就完了。这个写法在小数据上确实能过,但它有三个致命问题。第一,长度到十万级的表达式,CPython 在构建抽象语法树时会因为嵌套过深直接抛出递归相关的异常,程序崩溃;第二,这道题本身考察的就是解析能力,用内建求值等于交白卷,即便通过了也没有任何训练价值;第三,很多评测环境对这类动态求值是有限制的。所以我从来不用,也不建议任何人用,哪怕只是用来"快速验证一下样例"。
第二个坑是把取模放在最后。前面已经算过,十万个乘号连起来,结果是个天文数字。有些人在 Python 里试了一下发现竟然能算出来,这是因为 Python 的整数是任意精度的,但那已经慢到无法接受,而且换到 C++ 就直接溢出。所以"先算完再取模"这条路在 C++ 里是死路,在 Python 里是慢路,两头都不通。
第三个坑是关于前导零的。题目说"当答案长度多于 4 位时,请只输出最后 4 位,前导 0 不输出"。这句话的意思是,如果结果是 4,就输出4,不要输出0004;如果结果是 0,就输出0。有些实现习惯性地用printf("%04d")补齐四位,这在第二个样例上就会输出7891(碰巧对),但在答案是 4 的时候输出0004,直接错。取模本身天然就满足"不输出前导零"的要求,所以只要老老实实用%d打印就行,不要画蛇添足。
3. 核心实现:状态机、代码与逐行验证
3.1 把扫描过程画成一个两状态机器
在写代码之前,我习惯先把扫描逻辑在脑子里建模。这道题的扫描器其实只有两个累积变量和一个数字缓冲:
ans:已完成加法项的和,初值 0。term:当前乘法链的乘积,初值 1(很重要,乘法的幺元是 1,不是 0)。num:正在读取的数字,初值 0。
每读到一个字符,走三条分支之一。读到数字,就num = num * 10 + (c - '0'),同时取模。读到乘号,就把num并进term,即term = term * num % MOD,然后把num清零,ans不动。读到加号,同样先term = term * num % MOD、num清零,然后把term结算进ans,最后把term重置为 1。整个扫描结束后,别忘记还有一个num没被并进去,要手动补一次同样的结算。
关键在于理解"乘号什么都不做"这一点。很多人第一次写的时候,读到乘号也去做结算,结果就变成了从左到右依次相乘相加的错误逻辑。实际上乘号的作用是"宣告下一个数字属于当前这条乘法链",它是连接符不是分隔符。真正起分隔作用的是加号,因为加号的优先级低,它标志着上一条乘法链的终止。
这里有个初值的细节值得单独提一句。term必须初始化为 1,因为它的语义是"当前乘法链的累积值",在还没读到任何数字之前,累积值应该是乘法的单位元。如果你图省事初始化为 0,那么第一次term * num永远是 0,从第一个数开始就全错了,而且错得很隐蔽——如果表达式恰好以 0 开头,你还看不出来。
3.2 C++ 版本与逐段说明
#include <cstdio> const int MOD = 10000; int main() { long long ans = 0; // 已完成加法项的累加和 long long term = 1; // 当前乘法链的累积乘积 long long num = 0; // 正在读取的数字 int c; while ((c = getchar()) != EOF && c != '\n') { if (c >= '0' && c <= '9') { num = (num * 10 + (c - '0')) % MOD; } else { // 遇到运算符,先把数字并进当前乘法链 term = term * num % MOD; num = 0; if (c == '+') { ans = (ans + term) % MOD; term = 1; // 开启新的一条乘法链 } // 若是 '*',term 继续累积,不做任何额外动作 } } // 收尾:最后一个数字还没有被并入 term = term * num % MOD; ans = (ans + term) % MOD; printf("%lld\n", ans); return 0; }读入方式我用的是getchar()逐字符读取。这个选择的原因是:输入只有一行、没有空格,cin >> s或者scanf("%s", buf)都能正常工作,但逐字符读有利于在一开始就把num取模这件事融进流程里,而且天然不受字符串缓冲区大小的影响。如果表达式长度上限被改到一千万,用getchar的版本连字符数组都不用开,空间是 O(1) 的。
while的循环条件写的是c != EOF && c != '\n',这里两个条件都要保留。只写!= EOF的话,如果输入的末尾有换行符,换行会被当作运算符进入else分支,虽然因为不是+所以只做了一次多余的term * num计算,但num此时已经是 0,乘出来还是 0,恰好不影响结果。不过这属于"侥幸正确",不是"逻辑正确",我宁愿把条件写全。至于行尾可能存在的\r(Windows 风格换行),它同样会落入else分支,行为跟换行一样,也不会出错,属于可以接受的小瑕疵。
num里面存的是已经取过模的数字。这一步会不会破坏正确性?前面已经论证过,数字只参与乘法和加法,取模是同态的,所以安全。term * num的两个因子都小于 10000,乘积最多八位数,long long完全扛得住,用int其实也够,但保持long long能省掉将来改题时的排查成本。
3.3 Python 版本与一个"作弊式"的对拍工具
MOD = 10000 def solve(s: str) -> int: ans = 0 term = 1 num = 0 for ch in s: if ch.isdigit(): num = (num * 10 + int(ch)) % MOD else: term = term * num % MOD num = 0 if ch == '+': ans = (ans + term) % MOD term = 1 term = term * num % MOD ans = (ans + term) % MOD return ans print(solve(input().strip()))Python 版本和 C++ 版本逻辑完全一致,只有一点需要注意:input()之后要strip(),把可能存在的首尾空白去掉,否则如果行尾有空格,ch会进入else分支,虽然结果不变,但逻辑上是脏的。
另外我想重点推荐一个"切分法",它只有几行,性能和内存都不如线性扫描(split会产生大量小字符串),但它极其直观,非常适合当对拍工具,用来验证你主写的那个版本。
MOD = 10000 s = input().strip() total = 0 for seg in s.split('+'): prod = 1 for x in seg.split('*'): prod = prod * int(x) % MOD total = (total + prod) % MOD print(total)这个版本把"分段求积再求和"的思路直接写成了代码,split('+')就是切段,内层split('*')就是段内求积。它的正确性是肉眼可见的,几乎不可能写错。所以我的建议是:主程序用线性扫描提交,本地再留一份切分法,随机生成小数据(比如长度 20 以内、数字 0-100、随机运算符),两边跑同样的输入比对结果。这样 A/B 两份实现在同一批测试数据上互相印证,能覆盖掉大量的边界情况。
3.4 复杂度与实测数据
时间复杂度是严格的 O(n),n 是字符串长度,每个字符恰好被处理一次,没有任何嵌套循环。空间复杂度在 C++ 版本里是 O(1),除了三个累加变量之外不需要额外空间;Python 版本因为存了整个字符串,是 O(n),但这是解释器层面的,算法本身仍然是常数额外空间。
我本地做过一次粗略的压测,生成一个长度为 100000 的全乘号表达式,C++ 版本跑下来耗时在 2 毫秒左右,几乎全是 IO 的时间。Python 版本在 20 毫秒上下,也远远够用。这个量级说明,对于本题十万的长度上限,任何 O(n) 的正确实现都不可能有性能问题,所以不要在常数优化上浪费时间,把精力放在正确性上。
4. 往上走一步:把栈写法和递归下降也啃下来
4.1 双栈法为什么值得单独练一遍
虽然本题用不上栈,但我还是建议你写一遍双栈版本。原因是它是一套可以反复复用的模板,一旦练熟,往后遇到带括号的四则运算、带一元负号、带整除的变种题,都能直接套用。它的核心思想非常优雅:用一个数字栈存操作数,用一个符号栈存待定的运算符,每次要执行运算时,只有当符号栈顶的运算符优先级不低于当前运算符时才结算,这样能保证高优先级的运算先完成。
#include <cstdio> #include <stack> using namespace std; stack<long long> nums; stack<char> ops; const int MOD = 10000; int rankOf(char op) { return op == '+' ? 1 : 2; } void calcOnce() { long long b = nums.top(); nums.pop(); long long a = nums.top(); nums.pop(); char op = ops.top(); ops.pop(); long long r = (op == '+') ? (a + b) % MOD : (a * b) % MOD; nums.push(r); } int main() { char c; long long num = 0; bool reading = false; while ((c = getchar()) != EOF && c != '\n') { if (c >= '0' && c <= '9') { num = (num * 10 + (c - '0')) % MOD; reading = true; } else { if (reading) { nums.push(num); num = 0; reading = false; } while (!ops.empty() && rankOf(ops.top()) >= rankOf(c)) calcOnce(); ops.push(c); } } if (reading) nums.push(num); while (!ops.empty()) calcOnce(); printf("%lld\n", nums.top()); return 0; }这段代码比线性扫描长了一倍多,但它的价值在于可扩展性。往后要加括号,只需要在遇到左括号时把它压进符号栈,遇到右括号时不断结算直到弹出左括号为止;要加减法,把calcOnce里的分支补上就行,注意减法不满足交换律,弹出顺序不能反。这些改动都是局部的,不需要重构整体结构。
4.2 递归下降:读起来最像文法的写法
还有一种写法,语法感更强,叫做递归下降。它的思路是完全按照表达式的语法分层来写函数:expr负责处理加法层,term负责处理乘法层,factor负责处理最底层的数字(或者括号里的表达式)。每一层只关心自己这一级的运算符,遇到更高级的运算就向下调用。
#include <cstdio> const int MOD = 10000; char buf[100005]; int pos = 0; long long factor() { long long v = 0; while (buf[pos] >= '0' && buf[pos] <= '9') { v = (v * 10 + (buf[pos] - '0')) % MOD; pos++; } return v; } long long term() { long long v = factor(); while (buf[pos] == '*') { pos++; v = v * factor() % MOD; } return v; } long long expr() { long long v = term(); while (buf[pos] == '+') { pos++; v = (v + term()) % MOD; } return v; } int main() { scanf("%s", buf); printf("%lld\n", expr()); return 0; }这段代码只有二十几行,但结构极其清晰,几乎是把文法定义抄成了代码。它的可读性在所有写法里是最好的,后期维护成本也最低。缺点是对括号的处理稍微麻烦一点——你需要在factor里判断当前字符是不是左括号,如果是就递归调用expr并消费掉右括号。这个代价对大多数场景来说是可以接受的。
递归深度方面不用太担心。在这道题里,expr调用term调用factor,深度是常数级,不会爆栈。只有在表达式嵌套极其深的括号版本里才需要考虑手动模拟栈。
4.3 从这道题延伸到工程场景
很多人刷完这种题就扔了,但表达式求值在工程里是真的常见。举几个我实际遇到过或者听说过的例子:一些持续集成工具允许你用表达式写条件判断,比如判断某个分支名匹配什么模式、某个变量满足什么条件才触发任务,背后就是一个小的表达式解析器;电子表格里的公式、报表系统里的自定义指标、游戏里的伤害计算公式配置,本质都是表达式求值;再复杂一点,数据库的查询优化器要解析WHERE子句,那也是表达式求值的一个变种,只不过带着类型系统和索引信息。
在这些场景里,你对"优先级"和"求值顺序"的理解会直接转化成代码质量。见过太多人写配置解析,直接把一整串表达式按空格劈开然后顺序计算,遇到a + b * c就得出错误结果。所以这道题虽然只有四十分的分值,但它训练的是一种结构化的思维习惯:先识别运算的层次,再决定处理顺序。这个习惯的价值远超题面本身。
| 工程场景 | 用到的核心能力 | 与本题的关系 |
|---|---|---|
| 配置文件的表达式条件 | 优先级判断、短路求值 | 优先级分层是同一套思路 |
| 报表自定义指标 | 数值求值、精度与取模 | 累加与取模策略直接复用 |
| 模板引擎的数据绑定 | 词法切分、状态机扫描 | 逐字符扫描的写法完全一样 |
| 计费规则配置 | 分段求和、分段求积 | 和"按加号切段"是同一个模型 |
5. 常见问题与排查技巧实录
5.1 提交前必看的踩坑速查表
下面这张表是我自己以及身边人在这道题上真实犯过的错误,按出现频率排序。每一条我都标注了"典型症状",方便你对着自己的错误输出去反查。
| 编号 | 错误写法 | 典型症状 | 正确做法 |
|---|---|---|---|
| 1 | term初始化为 0 | 无论输入什么都输出 0,或只输出第一段的和 | 初始化为 1 |
| 2 | 只在最后取一次模 | C++ 上大数据点溢出成负数或乱码 | 每次乘、每次加都取模 |
| 3 | 用%04d补齐四位 | 答案为 4 时输出0004 | 普通%d,不做补齐 |
| 4 | 忘记结算最后一个数字 | 结果总是少最后一项 | 循环结束后补一次结算 |
| 5 | 用int存中间乘积 | 极端数据下溢出为负 | 用long long |
| 6 | 读到乘号也去结算ans | 变成从左到右依次算,第一组样例输出 10 | 乘号不结算,只并进term |
| 7 | 用eval或内建求值 | 长字符串崩溃或超时 | 手写扫描 |
| 8 | 用递归拆分求和导致深度过大 | 十万个加号时栈溢出 | 迭代扫描 |
| 9 | 读入时漏掉整行,只读到第一个空格 | 表达式被截断 | 用getchar或读整行 |
表格第 1 条和第 4 条是最常见的两个,我几乎每次带人做这道题都会看到。第 1 条的症状特别有辨识度:如果你的程序在1+1*3+4上输出0,那基本就是term初值设错了,或者你在某处把term重置成了 0 而不是 1。第 4 条的症状是"总是差最后一项",比如1+1*3+4输出4而不是8,这说明你的收尾结算漏了。
第 6 条值得单独展开。它的错误代码长得很像正确代码,只是多了一行:
if (c == '+') { ans = (ans + term) % MOD; term = 1; } if (c == '*') { ans = (ans + term) % MOD; term = 1; } // 错这样写的话,1+1*3+4会被算成1+1得 2,*3时先结算 2 再重置,最后变成 2+3+4=9,如果恰好没输出 10 说明还漏了别的。判断依据很简单:正确逻辑下,ans只在遇到加号和结束时才会更新,你在代码里搜一下ans =,如果它出现在了乘号分支里,那就是错的。
5.2 用对拍和构造数据做自证
我个人的习惯是,任何涉及边界处理的题,写完一定要跑对拍,不能只靠样例。对拍的具体做法是写一个数据生成器,用脚本循环生成小数据,然后同时跑主程序和一个可信的慢速实现(比如前面那个split切分法),比对两边输出。下面是一个生成器的样子,用 Python 写起来最快:
import random n = random.randint(1, 8) # 乘法链段数 parts = [] for _ in range(n): m = random.randint(1, 3) # 段内因子个数 parts.append('*'.join(str(random.randint(0, 1000)) for _ in range(m))) print('+'.join(parts))这个生成器会产出类似12*34+5+678*9*1这样的表达式。跑个几千轮,如果两边结果全一致,基本可以认为主实现是对的。除了随机对拍,还要手工构造几个极端用例:全 0(0、0*0+0)、单个数字(12345)、超长数字(2147483647)、结果为 10000 的整数倍(这种情况下输出应该是0,注意不是空行)、只有一个乘号(9999*9999,真实值是 99980001,后四位是 1)。
其中"结果为 10000 整数倍"这一条特别值得测。比如输入10000,正确输出是0。如果你的程序输出空行,说明你在某处做了"如果为 0 就不输出"的判断,这在别的题里可能成立,在这里是错的。同理,输入0*0输出也应该是0。
5.3 评测环境里的非算法问题
有几个跟算法无关但确实会让人卡住的问题,我也记录一下。第一是输入行的结尾格式,有些环境用的是 CRLF,也就是行尾有个\r,如果你用getchar读到\r时直接当运算符处理,虽然本题不影响结果,但逻辑上不严谨;如果你的实现里对非法字符做了断言,就会挂。稳妥的做法是遇到非数字非运算符的字符就跳过。
第二是输出末尾的换行。绝大多数评测环境对末尾有没有换行是宽容的,但总有一小部分严格比对最后一行,所以printf("%lld\n", ans)比printf("%lld", ans)更保险。这个成本几乎为零,没理由不做。
第三是关于空输入。题目保证输入非空,但你本地调试的时候有可能不小心敲了空行,程序会输出1(因为term初值是 1,收尾时num是 0,1*0得 0,0+0还是 0,其实是 0)。这类"本地意外"不用太在意,但如果你在写更通用的解析器,就该显式处理空输入。
第四点我想说说调试方法。这道题的输出只有一行一个数,出错了很难定位。我的做法是加一个临时的调试开关,把每一步的ans、term、num、c都打出来,跑一遍那个错掉的样例,肉眼比对。往往一眼就能看出是哪一步的状态转移错了。这个土办法比任何高级调试器都快,尤其是在你只有一个小样例的时候。
// 临时调试输出,提交前务必删掉 fprintf(stderr, "c=%c num=%lld term=%lld ans=%lld\n", c, num, term, ans);用stderr而不是stdout是个小技巧,这样调试信息和答案不会混在一起,你可以只看标准输出里的答案。
5.4 从这道题带走的几条经验
最后说几条我个人觉得最有普适价值的体会。第一,限制条件是解题的入口,不是障碍。*、+、无括号、取后四位,这四条限制每一条都在告诉你一种简化路径。读题时把限制条件一条条列出来,然后问自己"如果没有这条限制会怎样",往往答案就藏在里面。第二,取模的可交换性要形成肌肉记忆:加法和乘法可以边算边取,减法和除法不行。这个判断能帮你在很多题上迅速做出决策,而不是每次都要推导一遍。
第三,永远写两个实现。哪怕主实现只有二十行,也值得再写一个完全不同的版本用来对拍。人的自信不可靠,机器的一致性才可靠。我在一次练习赛里就吃过亏,自认为逻辑天衣无缝的那个实现,因为term重置位置放错了一行,在某个只有两个运算符的用例上挂了,而样例全过。从那以后我基本不再相信"我这个逻辑肯定没问题"这句话了。
第四,把简单题写干净。这道题的正确写法二十行就够,但哪怕只有二十行,也值得加上清晰的变量命名和注释。term和ans这两个名字是有讲究的,它们表达的是"当前项"和"累计和"这两个不同的语义,如果都叫sum或者都叫tmp,调试的时候你的脑子会乱。写代码时多花的这几秒钟,在排查问题时会成倍地还给你。
至于扩展方向,如果你觉得这道题不过瘾,可以按这个顺序往上加难度:先加括号,用双栈或递归下降都能处理;再加减号,注意取模修正;然后加除法和整除,这时候要开始考虑除零和取模下逆元的问题;最后加一元负号和多位数常量里的十六进制前缀,基本就是一个能用的小型计算器内核了。这条路走完,你再看任何配置解析、公式引擎的源码,都会觉得亲切。