正则表达式匹配
问题简介
题目描述
给你一个字符串 s 和一个字符规律 p,请你来实现一个支持 '.' 和 '*' 的正则表达式匹配。
'.'匹配任意单个字符'*'匹配零个或多个前面的那一个元素
所谓匹配,是要涵盖整个 字符串 s 的,而不是部分字符串。
示例说明
✅ 示例 1:
输入:s = "aa", p = "a"
输出:false
解释:"a" 无法匹配 "aa" 整个字符串。
✅ 示例 2:
输入:s = "aa", p = "a*"
输出:true
解释:因为 '*' 代表可以匹配零个或多个前面的那一个元素, 在这里前面的元素就是 'a'。因此,字符串 "aa" 可被视为 'a' 重复了一次。
✅ 示例 3:
输入:s = "ab", p = ".*"
输出:true
解释:".*" 表示可匹配零个或多个('*')任意字符('.')。
✅ 示例 4:
输入:s = "aab", p = "c*a*b"
输出:true
解释:因为 '*' 表示零个或多个,这里 'c' 为 0 个, 'a' 被重复一次。因此可以匹配字符串 "aab"。
✅ 示例 5:
输入:s = "mississippi", p = "mis*is*p*."
输出:false
解题思路
方法一:动态规划(推荐)
💡 核心思想: 使用二维 DP 表来记录子问题的解。
步骤分析:
-
定义状态:
dp[i][j]表示字符串s的前i个字符和模式p的前j个字符是否匹配。 -
初始化:
dp[0][0] = true:空字符串和空模式匹配- 对于模式
p,如果p[j-1] == '*',则dp[0][j] = dp[0][j-2](*可以匹配零个前面的字符)
-
状态转移:
- 如果
p[j-1]不是'*':- 当
s[i-1] == p[j-1]或p[j-1] == '.'时,dp[i][j] = dp[i-1][j-1]
- 当
- 如果
p[j-1] == '*':- 匹配零个:
dp[i][j] = dp[i][j-2] - 匹配一个或多个: 如果
s[i-1] == p[j-2]或p[j-2] == '.',则dp[i][j] = dp[i-1][j]
- 匹配零个:
- 如果
-
返回结果:
dp[m][n],其中m和n分别是s和p的长度。
方法二:递归 + 记忆化
💡 核心思想: 直接按照匹配规则递归处理,使用记忆化避免重复计算。
步骤分析:
-
基本情况:
- 如果模式为空,返回字符串是否为空
- 如果字符串为空,检查模式是否能匹配空字符串(如
a*b*c*)
-
递归情况:
- 检查第一个字符是否匹配
- 如果模式的第二个字符是
'*':- 跳过
x*组合(匹配零个) - 或者匹配一个字符并继续匹配剩余字符串(匹配一个或多个)
- 跳过
- 否则,直接匹配第一个字符并递归处理剩余部分
方法三:纯递归(会超时)
❌ 不推荐: 时间复杂度指数级,仅用于理解问题逻辑。
// 方法一:动态规划
java
class Solution {
public boolean isMatch(String s, String p) {
int m = s.length(), n = p.length();
boolean[][] dp = new boolean[m + 1][n + 1];
// 初始化
dp[0][0] = true;
for (int j = 2; j <= n; j += 2) {
if (p.charAt(j - 1) == '*') {
dp[0][j] = dp[0][j - 2];
}
}
// 状态转移
for (int i = 1; i <= m; i++) {
for (int j = 1; j <= n; j++) {
char sc = s.charAt(i - 1);
char pc = p.charAt(j - 1);
if (pc != '*') {
if (sc == pc || pc == '.') {
dp[i][j] = dp[i - 1][j - 1];
}
} else {
// '*' 匹配零个前面的字符
dp[i][j] = dp[i][j - 2];
// '*' 匹配一个或多个前面的字符
if (sc == p.charAt(j - 2) || p.charAt(j - 2) == '.') {
dp[i][j] = dp[i][j] || dp[i - 1][j];
}
}
}
}
return dp[m][n];
}
}
// 方法二:递归 + 记忆化
java
class Solution2 {
private Boolean[][] memo;
public boolean isMatch(String s, String p) {
memo = new Boolean[s.length() + 1][p.length() + 1];
return dfs(s, 0, p, 0);
}
private boolean dfs(String s, int i, String p, int j) {
if (memo[i][j] != null) {
return memo[i][j];
}
boolean result;
if (j == p.length()) {
result = i == s.length();
} else {
boolean firstMatch = i < s.length() &&
(s.charAt(i) == p.charAt(j) || p.charAt(j) == '.');
if (j + 1 < p.length() && p.charAt(j + 1) == '*') {
result = dfs(s, i, p, j + 2) ||
(firstMatch && dfs(s, i + 1, p, j));
} else {
result = firstMatch && dfs(s, i + 1, p, j + 1);
}
}
memo[i][j] = result;
return result;
}
}
// 方法一:动态规划
go
func isMatch(s string, p string) bool {
m, n := len(s), len(p)
dp := make([][]bool, m+1)
for i := range dp {
dp[i] = make([]bool, n+1)
}
// 初始化
dp[0][0] = true
for j := 2; j <= n; j += 2 {
if p[j-1] == '*' {
dp[0][j] = dp[0][j-2]
}
}
// 状态转移
for i := 1; i <= m; i++ {
for j := 1; j <= n; j++ {
sc, pc := s[i-1], p[j-1]
if pc != '*' {
if sc == pc || pc == '.' {
dp[i][j] = dp[i-1][j-1]
}
} else {
// '*' 匹配零个前面的字符
dp[i][j] = dp[i][j-2]
// '*' 匹配一个或多个前面的字符
if sc == p[j-2] || p[j-2] == '.' {
dp[i][j] = dp[i][j] || dp[i-1][j]
}
}
}
}
return dp[m][n]
}
// 方法二:递归 + 记忆化
go
func isMatch2(s string, p string) bool {
memo := make(map[string]bool)
return dfs(s, 0, p, 0, memo)
}
func dfs(s string, i int, p string, j int, memo map[string]bool) bool {
key := fmt.Sprintf("%d,%d", i, j)
if val, exists := memo[key]; exists {
return val
}
var result bool
if j == len(p) {
result = i == len(s)
} else {
firstMatch := i < len(s) && (s[i] == p[j] || p[j] == '.')
if j+1 < len(p) && p[j+1] == '*' {
result = dfs(s, i, p, j+2, memo) ||
(firstMatch && dfs(s, i+1, p, j, memo))
} else {
result = firstMatch && dfs(s, i+1, p, j+1, memo)
}
}
memo[key] = result
return result
}
示例演示
📌 以 s = "aab", p = "c*a*b" 为例:
| i\j | "" | c | * | a | * | b |
|---|---|---|---|---|---|---|
| "" | T | F | T | F | T | F |
| a | F | F | F | T | T | F |
| a | F | F | F | T | T | F |
| b | F | F | F | F | F | T |
✅ 最终结果:dp[3][5] = true
答案有效性证明
正确性分析:
-
基础情况正确:
- 空字符串与空模式匹配 ✅
- 模式中
x*可以匹配零个字符 ✅
-
状态转移完整:
- 处理了普通字符匹配(包括
'.') - 处理了
'*'的两种情况:匹配零个和匹配多个 - 所有可能的匹配路径都被考虑
- 处理了普通字符匹配(包括
-
数学归纳法:
- 假设对于所有
i' < i和j' < j,dp[i'][j']计算正确 - 根据状态转移方程,
dp[i][j]也能正确计算
- 假设对于所有
复杂度分析
| 方法 | 时间复杂度 | 空间复杂度 | 说明 |
|---|---|---|---|
| 动态规划 | O(m×n) | O(m×n) | m, n 分别为字符串和模式长度 |
| 递归+记忆化 | O(m×n) | O(m×n) | 每个状态只计算一次 |
| 纯递归 | O(2^(m+n)) | O(m+n) | 指数级,会超时 ❌ |
💡 优化空间: 动态规划可以优化到 O(n) 空间复杂度,因为每次只依赖前一行的状态。
问题总结
✅ 关键要点:
'*'是最复杂的部分,需要考虑匹配零个和匹配多个两种情况- 动态规划的状态定义要清晰:
dp[i][j]表示前缀匹配 - 初始化很重要,特别是处理模式开头的
x*组合
✅ 常见陷阱:
- 忘记
'*'必须和前面的字符一起考虑 - 边界条件处理不当(空字符串、空模式)
- 没有考虑
'.'的特殊匹配规则
✅ 适用场景:
- 字符串模式匹配问题
- 需要处理通配符的场景
- 正则表达式引擎的基础实现
github地址: https://github.com/swf2020/LeetCode-Hot100-Solutions