正则表达式匹配
1.题目描述
给你一个字符串 s
和一个字符规律 p
,请你来实现一个支持 '.'
和 '*'
的正则表达式匹配。
'.'
匹配任意单个字符'*'
匹配零个或多个前面的那一个元素
所谓匹配,是要涵盖 整个 字符串 s
的,而不是部分字符串。
示例 1:
arduino
输入: s = "aa", p = "a"
输出: false
解释: "a" 无法匹配 "aa" 整个字符串。
示例 2:
arduino
输入: s = "aa", p = "a*"
输出: true
解释: 因为 '*' 代表可以匹配零个或多个前面的那一个元素, 在这里前面的元素就是 'a'。因此,字符串 "aa" 可被视为 'a' 重复了一次。
示例 3:
arduino
输入: s = "ab", p = ".*"
输出: true
解释: ".*" 表示可匹配零个或多个('*')任意字符('.')。
提示:
1 <= s.length <= 20
1 <= p.length <= 20
s
只包含从a-z
的小写字母。p
只包含从a-z
的小写字母,以及字符.
和*
。- 保证每次出现字符
*
时,前面都匹配到有效的字符
2.解决方案
1.暴力递归解法
- 思路:
-
从字符串
s
和模式p
的开头开始匹配。 -
对于模式
p
中的每个字符,分情况讨论:-
如果
p
的当前字符不是'*'
,那么s
和p
当前字符必须匹配(s
当前字符存在且与p
当前字符相等或者p
当前字符为'.'
),然后继续匹配下一个字符。 -
如果
p
的当前字符是'*'
,则有两种情况:'*'
前面的字符匹配0
次,此时跳过p
中'*'
及其前面的字符,继续匹配p
的下一个字符。'*'
前面的字符匹配至少1
次,前提是s
当前字符与'*'
前面的字符匹配(s
当前字符存在且与'*'
前面的字符相等或者'*'
前面的字符为'.'
),然后s
指针后移一位,继续尝试匹配。
-
当
s
和p
都匹配完时,返回true
;否则,返回false
。
-
- 代码实现:
ts
function isMatch(s: string, p: string): boolean {
if (!p) return!s;
let firstMatch = s && (s[0] === p[0] || p[0] === '.');
if (p.length >= 2 && p[1] === '*') {
return isMatch(s, p.slice(2)) || (firstMatch && isMatch(s.slice(1), p));
} else {
return firstMatch && isMatch(s.slice(1), p.slice(1));
}
}
- 分析:
- 时间复杂度 :在最坏情况下,时间复杂度为指数级 (O(2^{m + n})),其中
m
和n
分别是s
和p
的长度。因为对于每个字符,都可能有两种决策('*'
的两种情况)。 - 空间复杂度 :(O(m + n)),这是由于递归调用栈的深度最大为
m + n
。
- 缺点:
- 时间复杂度高,对于较长的字符串和模式,效率极低,容易超时。
2.动态规划解法
- 思路:
-
使用一个二维数组
dp[i][j]
表示s
的前i
个字符和p
的前j
个字符是否匹配。 -
初始化
dp[0][0] = true
,表示两个空字符串是匹配的。 -
对于
dp[i][j]
的计算: -
如果
p[j - 1]
不是'*'
,那么dp[i][j]
为true
当且仅当dp[i - 1][j - 1]
为true
且s[i - 1]
与p[j - 1]
匹配(s[i - 1]
存在且与p[j - 1]
相等或者p[j - 1]
为'.'
)。-
如果
p[j - 1]
是'*'
,则有两种情况:'*'
前面的字符匹配0
次,此时dp[i][j] = dp[i][j - 2]
。'*'
前面的字符匹配至少1
次,前提是s[i - 1]
与p[j - 2]
匹配(s[i - 1]
存在且与p[j - 2]
相等或者p[j - 2]
为'.'
),此时dp[i][j] = dp[i - 1][j]
。
-
- 代码实现:
ts
function isMatch(s: string, p: string): boolean {
const m = s.length;
const n = p.length;
const dp: boolean[][] = new Array(m + 1).fill(false).map(() => new Array(n + 1).fill(false));
dp[0][0] = true;
for (let j = 1; j <= n; j++) {
if (p[j - 1] === '*') {
dp[0][j] = dp[0][j - 2];
}
}
for (let i = 1; i <= m; i++) {
for (let j = 1; j <= n; j++) {
if (p[j - 1] === '.' || p[j - 1] === s[i - 1]) {
dp[i][j] = dp[i - 1][j - 1];
} else if (p[j - 1] === '*') {
dp[i][j] = dp[i][j - 2];
if (p[j - 2] === '.' || p[j - 2] === s[i - 1]) {
dp[i][j] = dp[i][j] || dp[i - 1][j];
}
}
}
}
return dp[m][n];
}
- 分析:
- 时间复杂度 :(O(m * n)),其中
m
和n
分别是s
和p
的长度。需要填充整个dp
数组。 - 空间复杂度 :(O(m * n)),用于存储
dp
数组。可以通过滚动数组优化到 (O(n)),因为dp[i][j]
只依赖于dp[i - 1][j]
和dp[i][j - 1]
以及dp[i - 1][j - 1]
。
- 优点:
- 相比暴力递归,时间复杂度大大降低,对于较长的字符串和模式,效率更高。
3.最优解及原因
- 最优解:
- 动态规划解法是最优解。
- 原因:
- 动态规划解法通过避免重复计算子问题,将时间复杂度从暴力递归的指数级 (O(2^{m + n})) 降低到 (O(m * n))。虽然空间复杂度也为 (O(m * n)),但在实际应用中,其效率的提升更为显著。滚动数组优化后的空间复杂度可进一步降低到 (O(n)),使得在处理长字符串时更具优势。