题目重点:逐字符状态机处理巨大字符串中的邮件 record,忽略展示名/注释内的分号,抽取并去重邮箱;明确未给的邮箱语法边界。
题意与假设
这道练习题保留了面经中值得训练的核心约束,并将需要澄清的条件单独列出。现场拿到变体时,先和面试官确认这些条件。
- 训练版引号包裹展示名、圆括号包裹注释,二者内容不作为邮箱 token;允许注释嵌套和内部反斜杠转义。
- 每条 record 最多一个 ASCII 常见邮箱,语法以代码正则为准;多个候选邮箱报错,空 record 跳过。
- 返回首次出现顺序去重的邮箱列表,大小写敏感;题目只说 list+dedupe,未规定排序或域名规范化。
- 不声称实现完整 RFC 5322、国际化邮箱、quoted local part 或任意嵌套引号规则。
解题路线
- 逐字符维护 quote:boolean 与 commentDepth:int;在引号/注释里,分号只是内容,遇转义时跳过被转义字符。
- 只在两状态均关闭时遇 ; 才提交当前 record;为避免注释里的假邮箱,外层扫描不把引号/注释字符写到候选缓冲。
- 对每条 record 的外层文本匹配一个训练语法邮箱;用 seen 集合去重,用 result 列表保留首见顺序。
- 若输入真的‘巨大’,当前实现仍会存全部 record;工程版在遇到有效分号时立即解析并清空缓冲,可把工作内存降到最长 record 加去重集合。
- 真实面试要先问括号是否可嵌套、引号中反斜杠规则、逗号/分号是否可出现在邮箱本身,再决定 lexer。
手动走一遍
'"A; B" <a@example.com>; (note; x) b@example.org; a@example.com' 只有引号/注释外两个分号起分隔作用,抽得 a@example.com 和 b@example.org,最后重复项不再输出。
Python 参考实现
下面的代码按上文假设实现,适合本地运行并追加自己的测试用例。
代码区可横向滚动;使用键盘时,先聚焦代码区再按方向键查看。
import re
EMAIL = re.compile(r'(?<![A-Za-z0-9._%+-])[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}(?![A-Za-z0-9.-])')
def unique_emails(log: str) -> list[str]:
"""Training grammar: one ASCII email per ; record; ignore quoted names/comments."""
outside = []
records = []
quote = False
comment_depth = 0
i = 0
while i < len(log):
char = log[i]
if (quote or comment_depth) and char == '\\' and i + 1 < len(log):
i += 2
continue
if quote:
if char == '"':
quote = False
elif comment_depth:
if char == '(':
comment_depth += 1
elif char == ')':
comment_depth -= 1
elif char == '"':
quote = True
elif char == '(':
comment_depth = 1
elif char == ')':
raise ValueError('unmatched comment close')
elif char == ';':
records.append(''.join(outside))
outside.clear()
else:
outside.append(char)
i += 1
if quote or comment_depth:
raise ValueError('unterminated quote/comment')
records.append(''.join(outside))
seen = set()
result = []
for record in records:
matches = EMAIL.findall(record)
if len(matches) > 1:
raise ValueError('multiple emails in one record')
if matches and matches[0] not in seen:
seen.add(matches[0])
result.append(matches[0])
return result
if __name__ == '__main__':
sample = '"A; B" <a@example.com>; (note; x) b@example.org; a@example.com'
assert unique_emails(sample) == ['a@example.com', 'b@example.org']
复杂度
状态扫描 O(L),正则在本训练语法的短 record 上处理;所有 record 物化后占 O(L),去重集合占 O(U) 个邮箱,U 为不同邮箱数。若改增量提交,可把临时文本内存降为 O(最长 record)。
易错点与追问
容易失分的边界
- 直接 split(';'),把展示名或注释中的分号误当 record 分隔。
- 只追踪引号、不追踪括号,或者遇转义引号就提前结束。
- 用全局 email 正则扫原串,会把注释里的假邮箱也抽出来。
- 把简化正则宣传为完整邮件协议解析器。
- 题目未明确排序规则,去重集合直接转 list 会产生不稳定顺序。
继续深挖
- 若真正支持 RFC quoted local-part,分号/注释如何与地址语法协同?
- 输入是分块流时,quote/comment 状态如何跨块保留?
- 去重邮箱数超过内存时,如何外部去重并保留首见顺序?
