剑指offer19.正则表达式匹配
题目描述:
给你一个字符串 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匹配,是不是相当于说p的前p.length()个字符和s的前s.length()个字符匹配呢,所以一个比较自然的想法就是dp[i][j]定义为p的前i个字符和s的前j个字符匹配(注意,p的第i个字符的下标是i-1,在之后从p中利用下标取字符的时候不要搞混了)。当然,还有一种方式是dp[i][j]定义为s的前i个字符和p的前j个字符匹配,这种方式无非是第一种定义的转置,但是在后续的遍历中第一种定义方式有一定好处。
二维数组dp[ ][ ]的大小是多少呢?从dp[0][0]一直到dp[n_p][n_s],如下代码所示:
int n_s = s.length();
int n_p = p.length();
bool dp[n_p + 1][n_s + 1];
动态规划的状态转移方程怎么写?
在写之前,我们不妨先看一个实例:
假设匹配串p=“.*ab”,待匹配串为s=“aabab”。
我们发现可以匹配,在画的表里面的体现是dp[4][5]是√,所以匹配成功。(p的长度为4,s的长度为5,所以最后结果为dp[4][5])
![](https://img-blog.csdnimg.cn/direct/d9db2ef5fbb947a89b7decaf5dd69053.png)
让我们来分析一下,这个表到底是怎么画的:
我们先来看如下几个问题:
(1)行号0和列号0都代表的是什么呢?
直观的理解是有可能是空串匹配空串,可以考虑这种特殊情况(虽然题目里面说s
、p长度都是>=1的,但是有可能样例里面有长度为0的,这个真的有可能)。
但是真正的目的是这样的:第0列表示可能会匹配空串,比如.*就可以匹配空串(如图中dp[2][0]所示);第0行倒是没有很明确的意义,一方面起到状态变量初始化的作用。另一方面在*进行匹配的时候是需要考虑到*的前面的字符和*的这个字符前面的字符的(比如a.*进行匹配的时候,就需要考虑.和a,因为.表示的是*可以匹配什么,a代表的是在在s中匹配到了a之后才能使用.*继续匹配)。
(2)状态变量的初始化是怎么样的呢?
第一行的dp[0][0]=true,后面的全是false,因为空串p可以匹配空串s,而空串p不能匹配非空串s。
(3)状态转移方程怎么写?
我们先来看一共有几种转态转移的可能?
2种:一种是英文字母a-z和. 另一种是*
a-z和.体现的是一次只能匹配一个字符,而*体现的是一次可以匹配0个或多个字符。
对于单字符匹配,我们先看表中第1、3、4行,发现第1行中,dp[1][1]=true,第3行中,dp[3][1] dp[3][2] dp[3][4] = true,第4行中,dp[4][3] dp[4][5] = true。发现规律了吗?
如果p的第i个字符和s的第j个字符匹配成功的话,dp[i][j]才有等于true的可能。那到底怎样才能等于true呢?需要满足dp[i-1][j-1] = true。这个可以理解为p的前i-1个元素和s和前j-1个元素匹配,p的第i个字符和s的第j个字符匹配成功,才能会得到p的前i个元素和s和前j个元素匹配。简化一下就是如果p的第i个字符和s的第j个字符匹配成功的话,dp[i][j] = dp[i-1][j-1]
所以对于单字符的情况,状态转移方程为:
match函数如下:
bool char_match(char a, char b){
return b == '.' || a == b; //a b相等 或 b是通配符.
}
对于*的匹配,我们看到表1中行号2的一行全是√,可能不是很好找规律,那么我们再找一个例子:
![](https://img-blog.csdnimg.cn/direct/63a052a947574536883a7d06c3ba38f8.png)
我们看行号3:dp[3][1] dp[3][2] dp[3][3] dp[3][4] = true
发现规律了吗?在baaab中首次出现a的前一个位置开始,如果有连续的a,那么连续打√,到最后一次出现a的位置为止。从a的前一个位置开始是因为a*可以匹配0个a。
这样就对了吗?还有一个前提条件:dp[1][1] = true,也就是如果第i行出现*,那么在i-2行有√的位置,第i行中才可以在对应位置打√。这样做的含义是a*表示a可以出现0次,那么如果p的第i个字符是*,那么p的前i-2个字符匹配s的前j个字符 可以推出 p的前i个字符匹配s的前j个字符,在这个实例中是b可以匹配b 可以推出 ba*能够匹配b。
所以*字符匹配匹配成功一共有2种情况:
(1)*匹配上了 0 个字符(比如ba*于b的匹配结果 和 b与b的匹配结果相同):
(2)*匹配上了多个字符(比如ba*已经匹配了ba,那么ba*也可以匹配baa):
代码
代码:
class Solution {
public:
bool char_match(char a, char b){
return b=='.' || a==b;
}
bool isMatch(string s, string p) {
int ns = s.length();
int np = p.length();
bool dp[np+1][ns+1];
for(int i=0;i<=np;++i){
for(int j=0;j<=ns;++j){
dp[i][j] = false;
}
}
dp[0][0] = true;
for(int i=1; i<=np; ++i){
if(p[i-1] == '*'){
for(int j=0;j<=ns;++j){
if(dp[i-2][j]) dp[i][j] = true;
if(j>0 && char_match(s[j-1], p[i-2]) && dp[i][j-1]) dp[i][j] = true;
}
} else {
for(int j=1;j<=ns;++j){
if(char_match(s[j-1], p[i-1])) dp[i][j] = dp[i-1][j-1];
}
}
}
return dp[np][ns];
}
};
我们来解释一下代码:
if(p[i-1] == '*'){
for(int j=0;j<=ns;++j){
if(dp[i-2][j]) dp[i][j] = true;
if(j>0 && char_match(s[j-1], p[i-2]) && dp[i][j-1]) dp[i][j] = true;
}
}
这里表示如果在p中读取到了*,那么有匹配0个字符和匹配多个字符两种情况,
匹配0个字符:就是if(dp[i-2][j]) dp[i][j] = true;
匹配多个字符:只有*前面的字符p[i-2](*是p[i-1],i表示p的第i个字符,因为p的下标是从0开始的)和s的第j个字符匹配,且dp[i][j-1]也匹配上了,才能推导出dp[i][j] = ture
注意:这里是俩if,用if-else也可以,毕竟匹配0个字符和匹配多个字符是不冲突的;
else {
for(int j=1;j<=ns;++j){
if(char_match(s[j-1], p[i-1])) dp[i][j] = dp[i-1][j-1];
}
}
这里表示如果在p中读取到了a-z或. 那么如果p[i-1] 匹配 s[j-1]的话,if(dp[i-1][j-1]=true) dp[i][j]=true,其实就是dp[i][j] = dp[i-1][j-1]
时间复杂度分析
动态规划遍历了dp二维数组,时间复杂度是O(MN),M=s.length(),N=p.length()
空间复杂度也是O(MN)
扩展学习
如果感觉你学会了,试一试这道题44. 通配符匹配 - 力扣(LeetCode)
这一道题的方法和我前面分析的一模一样,代码如下所示:
class Solution {
public:
bool char_match(char a, char b){
return b=='?' || a==b;
}
bool isMatch(string s, string p) {
int ns = s.length();
int np = p.length();
bool dp[np+1][ns+1];
for(int i=0;i<=np;++i){
for(int j=0;j<=ns;++j){
dp[i][j] = false;
}
}
dp[0][0] = true;
for(int i=1; i<=np; ++i){
if(p[i-1] == '*'){
for(int j=0;j<=ns;++j){
if(dp[i-1][j]) dp[i][j] = true;
if(j>0 && dp[i][j-1]) dp[i][j] = true;
}
} else {
for(int j=1;j<=ns;++j){
if(char_match(s[j-1], p[i-1])) dp[i][j] = dp[i-1][j-1];
}
}
}
return dp[np][ns];
}
};
最后:有什么好的想法和问题欢迎随时与我交流~~~