正则表达式匹配
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 <= 201 <= p.length <= 20s只包含从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)),使得在处理长字符串时更具优势。