题目重点:用expression/term/factor文法处理优先级,明确定义向零取整,拒绝隐式乘法和残缺表达式。
题意与假设
这道练习题保留了面经中值得训练的核心约束,并将需要澄清的条件单独列出。现场拿到变体时,先和面试官确认这些条件。
- 训练允许ASCII整数、空白、+−*/、括号与一元+−,不支持浮点、变量或隐式乘法。
- 除法向零取整,除零报错;不把Python负数//的向下取整当默认题意。
- 所有输入必须被完整消费,空式与语法错误报ValueError。嵌套过深会触及Python递归限制,可改显式栈。
解题路线
- 文法分三层:expression为term之间的加减,term为factor之间的乘除,factor为可带一元符号的整数或括号式。优先级由调用层级表达,不能见到运算符就按字符先后全部左算。
- 同一层用循环从左向右组合,保持二元运算左结合;8/3*3先得2再得6,不能把右半部分递归成3*3再除。括号内部重新调用expression直到遇右括号。
- 一元符号只在factor开始读,比如2*−3与−(1+2)。本文连续符号按奇偶翻转sign,1−−2也合法;如正式只允许单个符号,须收紧语法。
- 整数向零除法取abs(a)//abs(b),再按符号恢复。不要先用float做除法再int,大整数可能损失精度甚至溢出;比较−7/3的预期可快速核定合同。
- 解析一个数后碰到另一个数或左括号,要保留位置直到外层完整检查,不能静默接受1 2或2(3)。缺右括号、空括号、尾运算符需要明确报错,而不是返回部分结果。
- 复杂度证明要按字符指针:每个token由一次对应层处理,position只向前;一次嵌套返回不重新扫描括号内容。正则预切token也可用,但不应在递归中不断复制整段子字符串。
- 手写测试先覆盖优先级、结合性、负商、空白、大整数、边界括号与非法式。独立对照用Python AST作为另一解析器,再按有理数向零截断求值,避免eval任意输入。
手动走一遍
2*−(3+4)+5结果−9;8/3*3结果6;−7/3结果−2,而Python −7//3为−3。2(3)不是乘法简写,1+和()不是合法式,均应拒绝。
Python 参考实现
下面的代码按上文假设实现,适合本地运行并追加自己的测试用例。
代码区可横向滚动;使用键盘时,先聚焦代码区再按方向键查看。
"""ASCII integer expression grammar, unary +/-, parentheses, truncating division.
No implicit multiplication. Empty/malformed expressions raise ValueError.
"""
def calculate(text):
position = 0
def spaces():
nonlocal position
while position < len(text) and text[position].isspace():
position += 1
def factor():
nonlocal position
spaces()
sign = 1
while position < len(text) and text[position] in '+-':
if text[position] == '-':
sign = -sign
position += 1
spaces()
if position < len(text) and text[position] == '(':
position += 1
value = expression()
spaces()
if position >= len(text) or text[position] != ')':
raise ValueError('missing closing parenthesis')
position += 1
return sign * value
start = position
while position < len(text) and '0' <= text[position] <= '9':
position += 1
if position == start:
raise ValueError('integer or parenthesis expected')
return sign * int(text[start:position])
def term():
nonlocal position
value = factor()
while True:
spaces()
if position >= len(text) or text[position] not in '*/':
return value
op = text[position]
position += 1
right = factor()
if op == '*':
value *= right
else:
if right == 0:
raise ZeroDivisionError('division by zero')
quotient = abs(value) // abs(right)
value = -quotient if (value < 0) != (right < 0) else quotient
def expression():
nonlocal position
value = term()
while True:
spaces()
if position >= len(text) or text[position] not in '+-':
return value
op = text[position]
position += 1
right = term()
value = value + right if op == '+' else value - right
answer = expression()
spaces()
if position != len(text):
raise ValueError('unexpected trailing input')
return answer
复杂度
忽略大整数算术成本时O(L)时间,递归栈O(D),D为括号/递归嵌套深度;中间整数位数由输入与运算增长决定,不能把任意精度乘除称作常量成本。
易错点与追问
容易失分的边界
- 把乘除与加减同级。
- 负数除法使用//直接取整。
- 只解析前半段而不检查尾部。
- 递归复制括号子串造成二次开销。
继续深挖
- 如果不允许unary怎样收紧factor?
- 怎样换为两个栈避免递归深度限制?
- 需要输出错误位置怎么保留token span?
- 允许浮点或变量后哪些合同要重新定?
