文章目录
一、题目信息
1. 题目描述
给定一个字符串 (s) 和一个字符模式 ( p)。实现支持
.
和*
的正则表达式匹配。
.
匹配任意单个字符。*
匹配零个或多个前面的元素。匹配应该覆盖整个字符串 (s) ,而不是部分字符串。
2. 说明
- s 可能为空,且只包含从 a-z 的小写字母。
- p 可能为空,且只包含从 a-z 的小写字母,以及字符
.
和*
。
3. 示例1
输入:
s = "aa"
p = "a"
输出:false
解释:"a"
无法匹配"aa"
整个字符串。
4. 示例2
输入:
s = "aa"
p = "a*"
输出:true
解释:'*'
代表可匹配零个或多个前面的元素, 即可以匹配'a'
。因此, 重复'a'
一次, 字符串可变为"aa"
。
5. 示例3
输入:
s = "ab"
p = ".*"
输出:true
解释:".*"
表示可匹配零个或多个('*'
)任意字符('.'
)。
6. 示例4
输入:
s = "aab"
p = "c*a*b"
输出:true
解释:'c'
可以不被重复,'a'
可以被重复一次。因此可以匹配字符串"aab"
。
7. 示例5
输入:
s = "mississippi"
p = "mis*is*p*."
输出:false
8. 题目来源
来自LeetCode
第10题----正则表达式匹配
二、题目解析
1. 分析题意
说句实话,刚上来我是没读懂这道题的,看了示例才知道,原来是这个样子的:
这道题中的
*
表示之前那个字符可以有0个,1个或是多个,就是说,字符串"a*b"
,可以表示"b"
或是"aaab"
,即a
的个数任意(可以是0个,可以是3个),可以从示例4看出。还有一个需要注意的点就是
".*"
表示可匹配零个或多个('*'
)任意字符('.'
),示例3
既然这是动态规划专题,我们就先考虑动态规划求解吧:)
2. 动态规划
2.1 思路
定义一个二维的dp
数组,其中dp[i][j]
表示s[0,i)
和p[0,j)
是否匹配,然后有下面三种情况:
dp[i][j] = dp[i - 1][j - 1]
,
if p[j - 1] != '*' && (s[i - 1] == p[j - 1] || p[j - 1] == '.');
dp[i][j] = dp[i][j - 2]
,
if p[j - 1] == '*' and the pattern repeats for 0 times;
dp[i][j] = dp[i - 1][j] && (s[i - 1] == p[j - 2] || p[j - 2] == '.')
,
if p[j - 1] == '*' and the pattern repeats for at least 1 times
2.2 状态转移方程
d p [ i ] [ j ] = { d p [ i − 1 ] [ j − 1 ] d p [ i ] [ j − 2 ] d p [ i − 1 ] [ j ] & & ( s [ i − 1 ] = = p [ j − 2 ] ∣ ∣ p [ j − 2 ] = = ′ . ′ ) dp[i][j] = \begin{cases} dp[i - 1][j - 1]\\ dp[i][j - 2]\\ dp[i - 1][j] \&\& (s[i - 1] == p[j - 2]\ ||\ p[j - 2] == '.') \end{cases} dp[i][j]=⎩⎪⎨⎪⎧dp[i−1][j−1]dp[i][j−2]dp[i−1][j]&&(s[i−1]==p[j−2] ∣∣ p[j−2]==′.′)
2.3 复杂度分析
时间复杂度:
O
(
n
2
)
O(n^2)
O(n2)
空间复杂度:
O
(
n
2
)
O(n^2)
O(n2)
2.4 代码实现
class Solution {
public:
// 用p匹配s
bool isMatch(string s, string p) {
// dp[i][j]==true 表示s的前i位和p的前j位是匹配的
vector< vector<bool> > dp(s.length() + 3, vector<bool>(p.length() + 3, false));
// 边界条件为i=0 j=0 j=1
// j=0时p为空串,只能匹配空串,其他默认为false
dp[0][0] = true;
// j=1时p只有一个字符,只能匹配一个字符,其他全部为false
dp[1][1] = s[0] == p[0] || p[0] == '.';
// i=0时s为空串,p只有类似a*b*c*d*这样的形式才可以成功匹配
for (int j = 2; j < p.length() + 1; j += 2)
{
// 前0位和前j-2位是匹配的,且第j位是*
dp[0][j] = dp[0][j - 2] && p[j - 1] == '*';
}
// i表示s的前i位,j表示p的前j为
for (int i = 1; i < s.length() + 1; i++)
{
for (int j = 2; j < p.length() + 1; j++)
{
if (p[j - 1] != '*')
{
// 前i-1位和前j-1位是匹配的,且第i位和第j位是匹配的
dp[i][j] = dp[i - 1][j - 1] && (s[i - 1] == p[j - 1] || p[j - 1] == '.');
}
else
{
//当p[j-1]出现0次时,前i位和前j-2位是匹配的
//当p[j-1]出现1次或多次时,第i位一定匹配第j-1位,且前i-1位一定和前j位是匹配的。
dp[i][j] = dp[i][j - 2] || dp[i - 1][j] && (s[i - 1] == p[j - 2] || p[j - 2] == '.');
}
}
}
return dp[s.length()][p.length()];
}
};
3. 递归
我们可以考虑最容易想到的递归解法的。
3.1 思路
-
若
p
为空,若s
也为空,返回true
,反之返回false
。 -
若
p
的长度为1,若s
长度也为1,且相同或是p
为’.'则返回true
,反之返回false
。 -
若
p
的第二个字符不为*
,若此时s为空返回false
,否则判断首字符是否匹配,且从各自的第二个字符开始调用递归函数匹配。 -
若
p
的第二个字符为*
,进行下列循环,条件是若s
不为空且首字符匹配(包括p[0]
为点),调用递归函数匹配s和去掉前两个字符的p
(这样做的原因是假设此时的星号的作用是让前面的字符出现0次,验证是否匹配),若匹配返回true
,否则s
去掉首字母(因为此时首字母匹配了,我们可以去掉s
的首字母,而p
由于星号的作用,可以有任意个首字母,所以不需要去掉),继续进行循环。 -
返回调用递归函数匹配
s
和去掉前两个字符的p
的结果(这么做的原因是处理星号无法匹配的内容,比如s="ab", p="a*b"
,直接进入while循环后,我们发现"ab"
和"b"
不匹配,所以s变成"b"
,那么此时跳出循环后,就到最后的return来比较"b"
和"b"
了,返回true
。再举个例子,比如s=""
,p="a*"
,由于s
为空,不会进入任何的if
和while
,只能到最后的return
来比较了,返回true
,正确)。
3.2 复杂度分析
时间复杂度:
O
(
n
2
)
O(n^2)
O(n2)
空间复杂度:
O
(
1
)
O(1)
O(1)
3.3 代码实现
class Solution {
public:
bool isMatch(string s, string p) {
if (p.empty())
return s.empty();
if (p.size() == 1)
{
return (s.size() == 1 && (s[0] == p[0] || p[0] == '.'));
}
if (p[1] != '*')
{
if (s.empty()) return false;
return (s[0] == p[0] || p[0] == '.') && isMatch(s.substr(1), p.substr(1));
}
while (!s.empty() && (s[0] == p[0] || p[0] == '.'))
{
if (isMatch(s, p.substr(2)))
return true;
s = s.substr(1);
}
return isMatch(s, p.substr(2));
}
};