1. 正则表达式匹配问题解析
正则表达式匹配是LeetCode Hot100中的经典难题(第10题),要求实现一个支持'.'和'*'的通配符匹配功能。这个问题在真实开发场景中非常实用,比如:
- 文本编辑器的查找替换功能
- 日志文件的模式过滤
- 表单输入验证
- 数据清洗时的模式匹配
核心难点在于处理''这个特殊字符,它表示匹配零个或多个前面的元素。比如模式串"ab"可以匹配:
- "ab"(1个a)
- "aab"(2个a)
- "b"(0个a)
2. 动态规划解法详解
2.1 状态定义
我们使用二维dp数组,dp[i][j]表示:
- 字符串s的前i个字符
- 模式p的前j个字符 是否匹配
2.2 状态转移方程
if p[j-1] == '*': # 情况1:*匹配0次(跳过前一个字符) # 情况2:*匹配1次或多次(当前字符匹配且前面也匹配) dp[i][j] = dp[i][j-2] or (match(s[i-1], p[j-2]) and dp[i-1][j]) else: # 普通字符匹配 dp[i][j] = match(s[i-1], p[j-1]) and dp[i-1][j-1]其中match函数处理'.'通配符:
def match(a, b): return a == b or b == '.'2.3 边界条件处理
- dp[0][0] = True(两个空字符串匹配)
- 处理模式串开头带*的特殊情况(如"*a"非法)
3. 完整Python实现
def isMatch(s: str, p: str) -> bool: m, n = len(s), len(p) dp = [[False] * (n + 1) for _ in range(m + 1)] dp[0][0] = True # 处理模式串开头带*的情况(如a*b*) for j in range(1, n + 1): if p[j-1] == '*': dp[0][j] = dp[0][j-2] for i in range(1, m + 1): for j in range(1, n + 1): if p[j-1] == '*': dp[i][j] = dp[i][j-2] or (self.match(s[i-1], p[j-2]) and dp[i-1][j]) else: dp[i][j] = self.match(s[i-1], p[j-1]) and dp[i-1][j-1] return dp[m][n] def match(a, b): return a == b or b == '.'4. 复杂度分析
- 时间复杂度:O(mn),需要填充m×n的DP表格
- 空间复杂度:O(mn),DP表格存储空间
- 优化空间:可降维到O(n)
5. 常见错误与调试技巧
5.1 边界条件遗漏
- 忘记处理空字符串与空模式的匹配
- 忽略模式串开头带*的情况
调试技巧:先单独测试边界用例,如s="", p="a*"
5.2 状态转移错误
- 混淆*匹配0次和多次的逻辑
- 忘记处理'.'的通配情况
调试技巧:打印DP表格,观察错误发生的位置
5.3 索引越界
- 访问dp[i-1][j-1]时未检查边界
- 处理*时未确保j>1
防御性编程:在访问前检查数组边界
6. 实际应用场景
- 日志分析:匹配特定模式的错误日志
# 匹配如"ERROR 2023: ..."的日志 pattern = r"ERROR \d{4}:.*"- 表单验证:检查邮箱格式
# 简单邮箱验证 pattern = r"^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$"- 代码重构:批量替换特定函数调用
# 将foo(x,y)替换为bar(y,x) pattern = r"foo\((\w+),(\w+)\)" replacement = r"bar(\2,\1)"7. 性能优化技巧
- 提前终止:发现不匹配时立即返回
- 记忆化搜索:递归解法中添加缓存
- 空间优化:滚动数组降低空间复杂度
- 模式预处理:检查显然不匹配的情况
优化后的空间O(n)版本:
def isMatch(s: str, p: str) -> bool: m, n = len(s), len(p) prev = [False] * (n + 1) prev[0] = True for j in range(1, n + 1): if p[j-1] == '*': prev[j] = prev[j-2] for i in range(1, m + 1): curr = [False] * (n + 1) for j in range(1, n + 1): if p[j-1] == '*': curr[j] = curr[j-2] or (match(s[i-1], p[j-2]) and prev[j]) else: curr[j] = match(s[i-1], p[j-1]) and prev[j-1] prev = curr return prev[n]8. 测试用例设计
完整测试应包含以下情况:
| 测试用例 | 预期结果 | 测试要点 |
|---|---|---|
| "aa", "a" | False | 完全匹配 |
| "aa", "a*" | True | *匹配 |
| "ab", ".*" | True | .*组合 |
| "aab", "cab" | True | *跳过字符 |
| "mississippi", "misisp*." | False | 复杂不匹配 |
| "", "a*" | True | 空字符串匹配 |
| "a", "" | False | 空模式匹配 |
9. 其他解法对比
9.1 回溯法
def isMatch(s: str, p: str) -> bool: if not p: return not s first_match = bool(s) and p[0] in {s[0], '.'} if len(p) >= 2 and p[1] == '*': return (isMatch(s, p[2:]) or # 匹配0次 (first_match and isMatch(s[1:], p))) # 匹配1+次 else: return first_match and isMatch(s[1:], p[1:])- 优点:代码简洁
- 缺点:指数级时间复杂度,重复计算
9.2 有限状态机
构建模式串的状态转移图:
- 将模式转换为NFA(非确定有限自动机)
- 模拟状态转移过程
- 更适合工业级正则引擎实现
- 面试中实现复杂度较高
10. 面试技巧
- 先说明暴力解法,再优化到DP
- 画图展示DP表格填充过程
- 主动讨论边界条件和特殊case
- 比较不同解法的时间/空间复杂度
- 提及实际应用场景展示工程思维
常见面试问题:
- 如何处理.*的贪婪匹配?
- 如果增加+或?操作符怎么修改算法?
- 如何优化空间复杂度?
我在实际面试中遇到过这个问题的多个变种,发现面试官最关注的是:
- 能否清晰定义状态和转移方程
- 对边界条件的考虑是否全面
- 能否从暴力解法自然过渡到DP优化
- 代码实现的整洁度和正确性