1. 正则表达式匹配问题解析
正则表达式匹配是算法面试中的经典难题,也是实际开发中经常遇到的核心功能。这道题目要求我们实现一个简化版的正则表达式匹配功能,支持"."和"*"两种特殊字符的匹配规则。
1.1 题目理解与需求分析
题目给定一个字符串s和一个模式串p,要求判断s是否能够被p匹配。其中:
- "."可以匹配任意单个字符
- "*"表示前面的字符可以出现零次或多次
例如输入s="aab",p="cab"时,输出true。因为"c*"可以匹配零个c,"a*"可以匹配两个a,最后的"b"直接匹配。
这个问题的难点在于"*"的处理,因为它可以匹配零次或多次,这使得我们需要考虑多种可能的匹配方式。在实际面试中,面试官通常会考察候选人能否清晰地分析问题并设计出高效的解决方案。
2. 动态规划解法详解
2.1 动态规划思路建立
我们使用二维数组dp来记录匹配状态,其中dp[i][j]表示s的前i个字符和p的前j个字符是否匹配。这种定义方式在字符串匹配问题中非常常见。
初始化时:
- dp[0][0] = true:两个空字符串匹配
- 处理p可以匹配空字符串的情况(如"ab"可以匹配空字符串)
2.2 状态转移方程分析
状态转移分为两种情况:
-
当p[j-1]不是"*"时:
- 前i-1和j-1个字符必须匹配
- 当前字符必须匹配(相等或p[j-1]是".")
-
当p[j-1]是"*"时:
- 可以选择匹配0个前面的字符(跳过"*"和它前面的字符)
- 或者匹配至少1个前面的字符(如果前面的字符匹配)
2.3 代码实现细节
cpp复制bool isMatch(string s, string p) {
int m = s.size(), n = p.size();
vector<vector<bool>> dp(m + 1, vector<bool>(n + 1, false));
dp[0][0] = true;
// 处理p可以匹配空字符串的情况
for (int j = 1; j <= n; ++j) {
if (p[j - 1] == '*') {
dp[0][j] = dp[0][j - 2];
}
}
for (int i = 1; i <= m; ++i) {
for (int j = 1; j <= n; ++j) {
if (p[j - 1] != '*') {
dp[i][j] = dp[i - 1][j - 1] && (p[j - 1] == s[i - 1] || p[j - 1] == '.');
} else {
dp[i][j] = dp[i][j - 2];
if (p[j - 2] == s[i - 1] || p[j - 2] == '.') {
dp[i][j] = dp[i][j] || dp[i - 1][j];
}
}
}
}
return dp[m][n];
}
3. 算法复杂度与优化
3.1 时间与空间复杂度
- 时间复杂度:O(mn),其中m和n分别是字符串s和模式串p的长度
- 空间复杂度:O(mn),来自dp数组的存储
3.2 空间优化思路
我们可以观察到dp[i][j]只依赖于左边、上边和左上方的值,因此可以将空间复杂度优化到O(n):
cpp复制bool isMatch(string s, string p) {
int m = s.size(), n = p.size();
vector<bool> dp(n + 1, false);
dp[0] = true;
for (int j = 1; j <= n; ++j) {
if (p[j - 1] == '*') {
dp[j] = dp[j - 2];
}
}
for (int i = 1; i <= m; ++i) {
bool pre = dp[0];
dp[0] = false;
for (int j = 1; j <= n; ++j) {
bool temp = dp[j];
if (p[j - 1] != '*') {
dp[j] = pre && (p[j - 1] == s[i - 1] || p[j - 1] == '.');
} else {
dp[j] = dp[j - 2];
if (p[j - 2] == s[i - 1] || p[j - 2] == '.') {
dp[j] = dp[j] || temp;
}
}
pre = temp;
}
}
return dp[n];
}
4. 常见问题与调试技巧
4.1 边界条件处理
-
空字符串匹配:
- 空字符串可以匹配空模式
- 空字符串可以匹配类似"ab"这样的模式
-
模式以"*"开头:
- 这种情况在实际正则表达式中是不合法的
- 题目假设输入都是合法的,所以不需要特别处理
4.2 调试技巧
-
打印dp表:
- 对于小例子,可以打印整个dp表来验证逻辑
- 特别关注dp[0][j]的初始化是否正确
-
测试用例设计:
- 包含各种"*"的组合
- 包含"."的特殊情况
- 边界情况(空字符串、单个字符等)
4.3 常见错误
-
数组越界:
- 访问p[j-2]时要确保j>=2
- 初始化dp数组时大小应为m+1和n+1
-
逻辑错误:
- 忘记处理"*"匹配0次的情况
- 在"*"匹配时,没有正确判断前一个字符是否匹配
5. 实际应用与扩展
5.1 实际开发中的应用
正则表达式匹配在以下场景中非常有用:
- 表单输入验证
- 日志文件分析
- 文本搜索与替换
- 编译器中的词法分析
5.2 算法扩展思路
-
支持更多正则表达式特性:
- "+"(匹配1次或多次)
- "?"(匹配0次或1次)
- 字符类(如[a-z])
- 锚点(^和$)
-
非贪婪匹配:
- 默认"*"是贪婪匹配
- 可以扩展支持"*?"这样的非贪婪匹配
-
回溯算法实现:
- 虽然效率不如动态规划
- 但代码更直观,适合理解问题本质
6. 面试技巧与准备建议
6.1 面试中的解题思路
-
先明确问题要求:
- 确认支持哪些特殊字符
- 确认边界条件的处理方式
-
从简单情况开始:
- 先考虑没有"*"的情况
- 然后加入"*"的处理
-
画图辅助:
- 画出dp表的填充过程
- 用具体例子验证思路
6.2 相关题目推荐
-
通配符匹配(Wildcard Matching):
- "?"匹配任意单个字符
- "*"匹配任意序列(包括空序列)
-
正则表达式引擎实现:
- 支持更多元字符
- 考虑性能优化
-
字符串匹配问题:
- KMP算法
- Boyer-Moore算法
7. 个人实战经验分享
在实际解决这个问题时,我发现以下几点特别重要:
-
先处理简单情况:
- 把没有"*"的情况先想清楚
- 这样更容易理清"*"的处理逻辑
-
测试驱动开发:
- 先写测试用例
- 然后逐步实现功能
-
可视化思考:
- 在纸上画出dp表的填充过程
- 这能帮助发现逻辑漏洞
-
性能考量:
- 虽然O(mn)的解法通常足够
- 但在实际应用中可能需要进一步优化
最后,这道题目虽然难度较高,但通过系统地分析和逐步实现,完全可以掌握。建议多练习类似的动态规划问题,培养解决问题的思维模式。