在题库里翻到“火星文计算”这个标题时,我第一反应是:这题怕是让把 a、b、c 直接替换成数字就完事了吧?真正动手之后我才发现,光是“连续火星数字怎么切分”这一条,就能把不少人卡在 60 分以下。今天把这套题用 Java、JS、Python 三语言完整复盘一遍,题目本身不复杂,但跨语言实现时踩到的细节问题相当值得记录。
1. 这是一道翻译题,不是一道天文题
1.1 题面规则:火星人用五进制,还把数字藏进了字母
我拿到的题面是这样定义的:火星文的数字不是 0~9,而是只有五个数字符号,分别用 a、b、c、d、e 表示十进制里的 0、1、2、3、4。运算符号和括号与地球上的写法完全一致,还是 +、-、*、/ 和左右括号。输入是一串由这些符号组成的火星文表达式,要求输出计算结果,但输出也必须用火星文表示。
这句话里藏着两个容易理解偏的坑。第一个坑是“五进制”,很多同学以为只是字符替换,把 a 换成 0、b 换成 1 就结束,实际上火星文里的数字是五进制数,比如字符串 “cb” 不是地球数字 3、1 拼起来,而是五进制数 “21”,换算成十进制是 2*5+1=11。第二个坑是“连续数字”,火星文没有小数点也没有逗号,“bc” 就是一个完整的五进制数,不是一个 b 一个 c 两个数,所以写解析器的时候必须把连续出现的数字字符当作一个 token 读出来,不能一个字符一个字符地单独处理。
题目分值写的是 100 分,但实际测试点不是只有一组样例。我在本地把官方样例跑通以后,以为直接提交就能过,结果第一次只拿了 30 多分,问题就出在没有处理多位数和空格上。这也是我今天为什么要写这篇复盘的原因。
1.2 真正的考点链
如果把这道题的“新卷”外壳拆掉,它考的核心链路是四条:
- 字符映射:在 a~e 与 0~4 之间建立一一对应关系。
- 词法解析:把连续数字字符识别成一个五进制数,把运算符和括号识别成独立 token。
- 中缀表达式求值:处理 + - * / 的优先级和括号,输出十进制整数。
- 进制转换:把十进制结果转成五进制,再映射回 a~e 输出。
这四条里,第 3 条是传统计算器题,第 2 条是字符串处理题,第 1 条和第 4 条是进制转换题。也就是说,“火星文计算”其实是一道综合题,单独会某一项都不够,必须全部串起来。
说句实在话,很多人第一眼觉得它难,是因为“火星文”这个包装比较唬人。一旦你看穿它本质上就是“自定义进制 + 中缀表达式求值”,思路就会清晰很多。你要是已经会写十进制整数计算器,那这道题就只多了一个字符映射层和一个进制转换层,难度没有本质提升。
用 Java、JS、Python 三语言写,还有一个额外考点:三种语言对整数除法的处理不一样。Java 的 / 直接向零取整,Python 的 // 是向下取整,JS 的 / 得到浮点数,如果不加处理,同一组测试用例三套代码可能得到三种结果。这个我在后面专门拆开讲。
1.3 最容易出现的错误理解:简单字符替换
我用一个对比来说明为什么“只替换”不可行。假设输入是:
text复制d * (b + c) + e / c
如果只做字符替换,可以把这个字符串替换成 3 * (1 + 2) + 4 / 2,然后把这段地球表达式交给已有的计算器去算,最后结果 11 再替换成火星文。问题在于,题目不会保证表达式里的每个数字都是单个火星字符。一旦输入变成:
text复制dcb * b
简单替换会把 dcb 当成 d、c、b 三个独立操作数,但它其实是五进制数 86。你只有专门写一段“连续读入数字字符”的词法解析代码,才能正确区分 dcb 是一个数字。所以“字符替换”只是整道题最外层的一小步,真正的核心在词法和语法解析上。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 核心算法:先把火星文变回地球数字,算完再变回去
2.1 一张映射表解决“读”和“写”
最省心的做法是在代码里维护一张统一映射表,我这里直接用一个字符串当表:
- 下标 0 对应字符 a,表示数字 0
- 下标 1 对应字符 b,表示数字 1
- 下标 2 对应字符 c,表示数字 2
- 下标 3 对应字符 d,表示数字 3
- 下标 4 对应字符 e,表示数字 4
这样读火星字符时,DIGITS.indexOf(ch) 就是它的十进制数值;写结果时,DIGITS.charAt(v) 就是把十进制一位数转回火星字符。在 JS 和 Python 里也是一样的思路。使用字符串而不是哈希表的原因是代码量最少,并且天然保证“映射表”的顺序就是五进制的位权顺序,不容易写错。
在实际工程里,我一般会把这种映射抽成一个独立函数,而不是散落在表达式求值代码里。因为后面如果要扩展支持更大进制,只需要换表,不用动算法。
2.2 五进制与十进制的双向互转
五进制转十进制的方法非常直观。比如火星字符串 “dcb”,从最左边开始,逐步累乘 5:
- 初始 value = 0
- 遇到 d(值为 3):value = 0*5 + 3 = 3
- 遇到 c(值为 2):value = 3*5 + 2 = 17
- 遇到 b(值为 1):value = 17*5 + 1 = 86
所以 “dcb” 是十进制 86。反过来,十进制转五进制用短除法,每次除以 5 取余数,逆序拼接。比如 86:
- 86 / 5 = 17 余 1
- 17 / 5 = 3 余 2
- 3 / 5 = 0 余 3
余数逆序是 3, 2, 1,对应 d、c、b,结果就是 “dcb”。这两个互转过程在三种语言里写法略有差异,但逻辑完全一致。
这里我建议你在测试时专门打印十进制中间值。很多时候你的计算逻辑没问题,但进制转换写反了,最后输出全是错的。先验证 toMars(11) 应该返回 cb,再验证 calculate("dcb*b") 的中间结果是不是 86*1,这样定位错误很快。
2.3 中缀表达式求值:双栈法的完整逻辑
火星文表达式是标准的中缀表达式,最适合快速实现的就是双栈法:一个操作数栈存数值,一个运算符栈存 +、-、*、/ 和括号。算法可以概括成这样:
- 读到数字 token,直接解析成十进制数字并压入操作数栈。
- 读到左括号,压入运算符栈。
- 读到右括号,一直弹出运算符并计算,直到遇到左括号,再把左括号弹掉。
- 读到运算符,先比较它和栈顶运算符的优先级,如果栈顶优先级大于等于当前优先级,就先把栈顶计算掉,再把当前运算符压栈。
- 表达式读完以后,把运算符栈里剩余的运算符依次弹出计算。
这里最关键的是“大于等于”这个判断。乘除优先级高于加减,所以同一个优先级范围内从左到右计算;当遇到 14 / 2 * 3 这种连续乘
