题目重点:将连续数字词法化为占位符,比较算式的结构;用 1+1=2 与 2+3=100 解释数字相等关系也不应保留。
题意与假设
这道练习题保留了面经中值得训练的核心约束,并将需要澄清的条件单独列出。现场拿到变体时,先和面试官确认这些条件。
- 训练版字符只允许 ASCII 数字和单字符 + - * / =;连续数字是一枚 NUMBER token。
- 比较运算符顺序和数字 token 位置,忽略每个数字的大小、位数以及两个位置是否写同一个数字。
- 负号在本训练版按运算符处理;若面试官要求一元负数,应把 tokenizer 语法扩展。
- 非法字符报错;空字符串可得到空 token 序列,题目没有定义。
解题路线
- 先写 lexer,把连续数字扫描成 NUMBER,而不是逐个字符替换;否则 300 与 1 会被误判不同长度模式。
- 对允许的每个运算符保留原字符和 token 类型;整个表达式变为形如 NUMBER,+,NUMBER,=,NUMBER 的序列。
- 比较两个 token 序列完全相等即可;如要在大量 preset patterns 中查找,可将 tuple 用作哈希键索引。
- 不要计算算式值,也不要给重复数字分配相同变量标号:1+1=2 与 2+3=100 被题目明确说成相同 pattern。
手动走一遍
1+1=2 词法化为 # + # = #;2+3=100 也一样,所以匹配。1-2 的模式 # - #,和 1+2 的 # + # 不匹配。单独的 300 是一个 # token,而不是三个数字 token。
Python 参考实现
下面的代码按上文假设实现,适合本地运行并追加自己的测试用例。
代码区可横向滚动;使用键盘时,先聚焦代码区再按方向键查看。
def expression_shape(text: str) -> tuple[tuple[str, str], ...]:
"""Training grammar: ASCII digit runs and one-character + - * / = operators."""
tokens = []
i = 0
while i < len(text):
char = text[i]
if "0" <= char <= "9":
end = i + 1
while end < len(text) and "0" <= text[end] <= "9":
end += 1
tokens.append(("number", "#"))
i = end
elif char in "+-*/=":
tokens.append(("operator", char))
i += 1
else:
raise ValueError("character outside training grammar")
return tuple(tokens)
def same_numeric_pattern(a: str, b: str) -> bool:
return expression_shape(a) == expression_shape(b)
if __name__ == "__main__":
assert same_numeric_pattern("1+1=2", "2+3=100")
assert not same_numeric_pattern("1+1=2", "1-2=3")
复杂度
单个字符串长 L,词法分析 O(L) 时间,token 序列最坏 O(L) 空间;两个序列比较 O(L)。将 p 个 preset 总字符量 S 预处理成哈希索引需要预期 O(S) 时间/空间。
易错点与追问
容易失分的边界
- 直接把每个数字字符换 #,导致 300 的模式变成 ###。
- 保留数字是否相同的约束,错误地判 1+1=2 与 2+3=100 不同。
- 解析并验证 1+1=2 的数学真值;题目明确不检查。
- 没询问空白、括号、一元负号的规则就宣称覆盖任意算式。
继续深挖
- 若空格可任意出现,tokenizer 应如何跳过或保留?
- 若允许括号、多字符运算符和一元负号,怎么写一个小型 lexer?
- 若 preset 很多而输入很多,如何避免每次线性扫描所有模式?
