LeetCode 第 10 题:正则表达式匹配
字数 2781 2025-10-26 02:26:28
好的,我们接下来学习 LeetCode 第 10 题:正则表达式匹配。
1. 题目描述
给你一个字符串 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
解释:".*" 表示可匹配零个或多个('*')任意字符('.')。
提示:
1 <= s.length <= 201 <= p.length <= 20s只包含从a-z的小写字母。p只包含从a-z的小写字母,以及字符.和*。- 保证每次出现字符
*时,前面都匹配到有一个有效字符。
2. 初步思路
这个问题难点在于 '*' 可以匹配 0 次 或 多次 前面的字符,这导致有多种匹配可能性。
例如 s = "aaa", p = "a*a":
"a*"可以匹配"aaa"的前两个'a',然后p剩下的'a'匹配最后一个'a'。- 或者
"a*"匹配"aaa"的前三个'a',但p剩下的'a'无法匹配(因为s用完了),这就不行。 - 或者
"a*"匹配 0 次'a',那么p = "a*a"就变成"aa"去匹配"aaa",显然不行。
所以我们需要尝试所有可能性,这提示用 动态规划(DP) 或 递归+记忆化。
3. 定义 DP 状态
设 dp[i][j] 表示:s 的前 i 个字符 与 p 的前 j 个字符 是否匹配。
i和j表示长度,即s[0..i-1]和p[0..j-1]。- 最终答案是
dp[m][n],其中m = s.length,n = p.length。
4. 状态转移方程
我们需要考虑 p 的第 j 个字符(即 p[j-1])的情况:
情况 1:p[j-1] 是普通字母(不是 '*')
- 那么必须
s[i-1] == p[j-1]才能匹配。 - 并且
s的前i-1个字符与p的前j-1个字符也要匹配。 - 即:
如果p[j-1]不是'*'且不是'.':
dp[i][j] = dp[i-1][j-1] and (s[i-1] == p[j-1])
情况 2:p[j-1] 是 '.'
'.'可以匹配任意单个字符,所以只要s还有字符,这里就能匹配。- 并且
s的前i-1个字符与p的前j-1个字符也要匹配。 - 即:
如果p[j-1] == '.':
dp[i][j] = dp[i-1][j-1]
情况 3:p[j-1] 是 '*'(重点)
这时 p[j-2] 是 '*' 前面的字符,记作 char(可能是字母或 .)。
'*' 表示匹配 0 次或多次 char。
子情况 3.1:匹配 0 次
- 即忽略
p的char*这两个字符(p中j-2和j-1位置)。 - 那么
dp[i][j] = dp[i][j-2]
子情况 3.2:匹配 1 次或多次
- 前提是
s[i-1]必须与char匹配(即char == '.'或char == s[i-1])。 - 如果匹配了
s[i-1],那么s的前i-1个字符应当与p的前j个字符匹配(因为'*'可以继续匹配前面的字符)。 - 即
dp[i][j] = dp[i-1][j] and (s[i-1] == p[j-2] or p[j-2] == '.')
合并两种情况:
dp[i][j] = dp[i][j-2] or (dp[i-1][j] and (s[i-1] == p[j-2] or p[j-2] == '.'))
注意:匹配多次的情况被 dp[i-1][j] 隐含了,因为从 i-1 到 i 时仍然用同一个 j(即 '*' 模式还在继续匹配)。
5. 初始条件
dp[0][0] = true:两个空字符串匹配。- 对于
i = 0(s为空)但j > 0:
只有当p的形如a*b*c*这样每个带'*'的字符都匹配 0 次时才可能为 true。
具体:如果p[j-1] == '*',则dp[0][j] = dp[0][j-2],否则为 false。 - 对于
j = 0且i > 0:dp[i][0] = false(空模式无法匹配非空字符串)。
6. 计算顺序
按 i 从 0 到 m,j 从 1 到 n(j=0 时只有 i=0 为 true,其余 false,可先初始化)。
7. 示例推导
s = "aa", p = "a*"
m=2,n=2- 初始化
dp[0][0] = true j=1:p[0]='a'不是'*',所以i=0时dp[0][1] = false(空s无法匹配"a")j=2:p[1]='*',char = p[0]='a'i=0:匹配 0 次:dp[0][2] = dp[0][0] = truei=1:- 匹配 0 次:
dp[1][0]不存在?不对,是dp[1][2] = dp[1][0] or ...但dp[1][0]是 false。
匹配 1 次以上:dp[0][2] and (s[0]=='a'=='a')→true and true→true。
所以dp[1][2] = false or true = true
- 匹配 0 次:
i=2:- 匹配 0 次:
dp[2][0]=false
匹配 1 次以上:dp[1][2] and (s[1]=='a'=='a')→true and true→true
所以dp[2][2] = false or true = true
- 匹配 0 次:
最终 dp[2][2] = true,匹配。
8. 代码实现(思路)
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
# 初始化空s匹配p的情况
for j in range(2, 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] == '*':
# 匹配0次
dp[i][j] = dp[i][j - 2]
# 匹配1次或多次
if p[j - 2] == '.' or p[j - 2] == s[i - 1]:
dp[i][j] = dp[i][j] or dp[i - 1][j]
elif p[j - 1] == '.':
dp[i][j] = dp[i - 1][j - 1]
else:
dp[i][j] = dp[i - 1][j - 1] and (s[i - 1] == p[j - 1])
return dp[m][n]
9. 总结
- 核心是处理
'*'的两种选择:匹配 0 次或匹配 1 次以上。 - 匹配 1 次以上的情况可以通过
dp[i-1][j]来递归包含多次匹配。 - 初始条件要注意空串与模式的匹配情况。
- 时间复杂度 O(mn),空间复杂度 O(mn)。