这是我参与新手入门的第2篇文章。
一、题目描述
给定一个字符串 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
解释:".*" 表示可匹配零个或多个('*')任意字符('.')。
思路分析
题目中的匹配是一个「逐步匹配」的过程:我们每次从字符串 p 中取出一个字符或者「字符 + 星号」的组合,并在 s 中进行匹配。对于 p 中一个字符而言,它只能在 s 中匹配一个字符,匹配的方法具有唯一性;而对于 p 中字符 + 星号的组合而言,它可以在 s 中匹配任意自然数个字符,并不具有唯一性。因此我们可以考虑使用动态规划,对匹配的方案进行枚举。
-
这道题考察了如何利用动态规划的思想来找到问题的最优解决方式,可用dp[i][j] 表示 s 的前 i 个是否能被 p 的前 j 个匹配。首先想的时候从已经求出了 dp[i-1][j-1] 入手,再加上已知 s[i]、p[j],要想的问题就是怎么去求 dp[i][j]。
-
第一个难想出来的点:怎么区分 * 的两种讨论情况首先给了 * ,明白 * 的含义是 匹配零个或多个前面的那一个元素,所以要考虑他前面的元素 p[j-1]。* 跟着他前一个字符走,前一个能匹配上 s[i],* 才能有用,前一个都不能匹配上 s[i],* 也无能为力,只能让前一个字符消失,也就是匹配 0 次前一个字符。
-
第二个难想出来的点:怎么判断前面是否匹配 dp[i][j] = dp[i-1][j] // 多个字符匹配的情况 or dp[i][j] = dp[i][j-1] // 单个字符匹配的情况 or dp[i][j] = dp[i][j-2] // 没有匹配的情况
-
思路有了接下来就是代码实现,由于本身从事前端行业对JavaScript比较熟悉所以就用JS代码来实现了
三、AC代码
const isMatch = (s, p) => {
if (s == null || p == null) return false;
const sLen = s.length, pLen = p.length;
const dp = new Array(sLen + 1);
for (let i = 0; i < dp.length; i++) {
dp[i] = new Array(pLen + 1).fill(false); // 将项默认为false
}
// base case
dp[0][0] = true;
for (let j = 1; j < pLen + 1; j++) {
if (p[j - 1] == "*") dp[0][j] = dp[0][j - 2];
}
// 迭代
for (let i = 1; i < sLen + 1; i++) {
for (let j = 1; j < pLen + 1; j++) {
if (s[i - 1] == p[j - 1] || p[j - 1] == ".") {
dp[i][j] = dp[i - 1][j - 1];
} else if (p[j - 1] == "*") {
if (s[i - 1] == p[j - 2] || p[j - 2] == ".") {
dp[i][j] = dp[i][j - 2] || dp[i - 1][j - 2] || dp[i - 1][j];
} else {
dp[i][j] = dp[i][j - 2];
}
}
}
}
return dp[sLen][pLen]; // 长sLen的s串 是否匹配 长pLen的p串
};
四、总结
本题的题目比较容易误导人们去研究如何进行字符串的匹配,从而忽略了使用动态规划算法来实现,但只要想到题目中的匹配是一个「逐步匹配」的过程,从已经求出了 dp[i-1][j-1] 入手,再加上已知 s[i]、p[j],要想的问题就是怎么去求 dp[i][j]就自然而然的想到了动态规划算法实现。