LeetCode10-正则表达式匹配
给定一个字符串 (s) 和一个字符模式 §。实现支持 ‘.’ 和 ‘*’ 的正则表达式匹配。
‘.’ 匹配任意单个字符。
‘*’ 匹配零个或多个前面的元素。
匹配应该覆盖整个字符串 (s) ,而不是部分字符串。
说明:
- s 可能为空,且只包含从 a-z 的小写字母。
- p 可能为空,且只包含从 a-z 的小写字母,以及字符 . 和 *。
示例 1:
输入:
s = "aa"
p = "a"
输出: false
解释: "a" 无法匹配 "aa" 整个字符串。
示例 2:
输入:
s = "aa"
p = "a*"
输出: true
解释: '*' 代表可匹配零个或多个前面的元素, 即可以匹配 'a' 。因此, 重复 'a' 一次, 字符串可变为 "aa"。
示例 3:
输入:
s = "ab"
p = ".*"
输出: true
解释: ".*" 表示可匹配零个或多个('*')任意字符('.')。
示例 4:
输入:
s = "aab"
p = "c*a*b"
输出: true
解释: 'c' 可以不被重复, 'a' 可以被重复一次。因此可以匹配字符串 "aab"。
示例 5:
输入:
s = "mississippi"
p = "mis*is*p*."
输出: false
一、思路
观察如下示例:
示例 4:
输入:
s = "aab"
p = "c*a*b"
输出: true
解释: 'c' 可以不被重复, 'a' 可以被重复一次。因此可以匹配字符串 "aab"。
匹配真正开始于第三个字符'a',之前的不匹配字符都跳过了
示例 5:
输入:
s = "mississippi"
p = "mis*is*p*."
输出: false
不匹配发生于第二个'*',因为一个'*'不能匹配'si'这两个字符,因此失败
总结一下’*'的替换规则:
- 字符’*'只能替换字符模式P中,出现在该字符之前的字符。
- 尽管’*'可以替换为多个字符,但是这多个字符都是相同的。
然后说下踩到的坑吧:
1、字符*的匹配规则
哎…理解错误做了半天发现通过不了,原来理解错了,假设字符*出现在第i个位置,那么,它只能替换为:
(1)空字符串
(2)任意个数的
p
[
i
−
1
]
p[i-1]
p[i−1]字符
也就是只能换成前一个位置的字符
2、这不是字符串匹配问题
我拿了一个例子来测试:
s="abcd"
p="abcabcabcabcabcd"
注意:s是p的后缀
输出:false
????我以为是输反了
然后又输了一次:
s="abcabcabcabcabcd"
p="abcd"
输出:false
还是false,这就说明对题目的理解出了问题
为了知道它的匹配规则,我又测试了别的例子:
例1:
s="aab"
p="ca*b"
输出:false
例2:
s="aab"
p="cc*a*b"
输出:false
例3:
s="aab"
p="c*a*b"
输出:true
例4:
s="aab"
p="*a*b"
输出:false
例5:
s="aab"
p="a*b."
输出:false
例6:
s="aab"
p="a*b*"
输出:true
例7:
s="aab"
p="a****************b"
输出:false
例8:
s="aab"
p="d*f*c*a*b"
输出:true
例9:
s=""
p="*"
输出:false
我好像明白了,规则是这样的:
(1)每个*(星花符)前面必须有一个字符(星花符除外),这个字符归属于星花符,就是说(字符x+星花符=0~任意个的字符x),在计算的时候,应该将这两个字符视为一个来算
(2)这个匹配要求的是一模一样,不能多,也不能少!!!
(一)错误的理解(懒得删了。。。不用看)
1、常规方法
与之前的字符串匹配很相似,就是一个一个对比,加入’*‘和’.'的匹配规则即可
整段代码写下来发现真的真的很复杂,其难点在于:
- 字符*到底应该匹配几个字符?
- 每次字符串发生失配时,我们之前建立好的字符*匹配表需要进行回溯,这个操作实现起来十分复杂
上面两个问题,我仅仅解决了第一个,第二个问题的解决方法有两个:
(1)回溯,按照字符s的失配字符开始,向前回溯,这个方法的最大问题可以用下面的例子描述:
假如,在第n次匹配中,遇到了字符s,我将其标志位置为1,表示*可以匹配该字符,但是随后发生了失配,于是我通过回溯将其置为0,但是我无法保证字符s是否出现在更前面的位置,所以这个方法是行不通
(2)全部清零,然后从字符串p的第一个字符开始直到我们需要匹配的第一个字符
这个方法的时间复杂度很高很高,基本行不通
基于以上两点,我放弃了一般的字符串匹配方法,改寻它法。
2、动态规划
经过(一)的尝试,我发现,字符串的匹配与之前的状态有关,考虑到贪心算法、回溯算法的特点都不适用,采用动态规划来试试。
设 f ( i , j ) f(i,j) f(i,j)表示长度为 i i i的字符串s与长度为 j j j的字符模式p是否匹配
这句话有几个隐藏的条件:
(1)若
f
(
i
,
j
)
=
=
1
f(i,j)==1
f(i,j)==1,则
j
>
=
i
j>=i
j>=i,且对于任意的
k
>
=
j
k>=j
k>=j,都有
f
(
i
,
k
)
=
=
1
f(i,k)==1
f(i,k)==1
(2)
f
(
0
,
j
)
=
=
1
f(0,j)==1
f(0,j)==1
通过上面的分析,我们知道刚刚对 f ( i , j ) f(i,j) f(i,j)的表述似乎有些不严谨,因为如果我们想用动态规划来求解的话,必须找到一个好的子结构问题,现在来看,这个定义存在一些缺点:
f ( i , j ) = ( f ( i , j − 1 ) + ( f ( i − 1 , j − 1 ) ∗ s [ i ] = = p [ k 1 ] ) + ( f ( i − 1 , j − 2 ) ∗ s [ i ] = = p [ k 2 ] ) . . . . . . . . ) f(i,j)=(f(i,j-1) +(f(i-1,j-1)*s[i]==p[k1])+(f(i-1,j-2)*s[i]==p[k2])........) f(i,j)=(f(i,j−1)+(f(i−1,j−1)∗s[i]==p[k1])+(f(i−1,j−2)∗s[i]==p[k2])........)
发现问题了吗?
根据这种表示,你根本找不到 s [ i ] s[i] s[i]应该与 p p p中的哪个位置的字符进行匹配,例如:
p=“abcabcabcabcabcabcabcd”
s=“abcd”
在这个例子中 f ( 3 , j ) = = 1 f(3,j)==1 f(3,j)==1对任意的 j > = 3 j>=3 j>=3都成立,然而你却很难进行下一步的匹配计算,例如计算 f ( 4 , 7 ) f(4,7) f(4,7)。
我们来算算这个结果,因为 f ( 3 , 7 ) = = 1 f(3,7)==1 f(3,7)==1而且 f ( 3 , 6 ) = = 1 f(3,6)==1 f(3,6)==1,我们需要匹配的字符是 s [ 4 ] s[4] s[4]即:字符串s的第四个字符,但是相对的来说,应该去p的什么地方取出这个与 s [ 4 ] s[4] s[4]匹配的字符呢?p[4]?还是p[5]?又或者是p[6]?
于是这个问题又变得复杂了
因为计算表达式 f ( i , j ) f(i,j) f(i,j)时,它仅仅告诉了你下一个需要匹配的字符是 s [ i ] s[i] s[i],而没有给出另一个字符的信息,这样显然不行
那么我们应该更改一下表达式的含义了:
设 f ( i , j ) f(i,j) f(i,j)表示长度为 i i i的字符串s与长度为 j j j的字符模式p的后 i i i个字符是否匹配
也就是说: f ( i , j ) = = 1 f(i,j)==1 f(i,j)==1等价于 s [ 1... i ] = p [ j − i + 1 , . . . , j ] s[1...i]=p[j-i+1,...,j] s[1...i]=p[j−i+1,...,j]
最终我们要知道是长度为m的字符串s与长度为n的字符模式p是否匹配,如果是匹配的,它是怎么得出来的?
考虑最后一步的情况:
-
f
(
m
−
1
,
n
−
1
)
=
1
f(m-1,n-1)=1
f(m−1,n−1)=1表示长度为
m
−
1
m-1
m−1的字符串s与长度为
n
−
1
n-1
n−1的字符模式p的后
m
−
1
m-1
m−1个字符匹配,此时还差最后一个字符,只需要比较最后一个字符看看是否相等,不相等则不匹配,这条路走不通了
你可能会说会说, - f ( m − 1 , n − 2 ) = 1 f(m-1,n-2)=1 f(m−1,n−2)=1表示长度为 m − 1 m-1 m−1的字符串s与长度为 n − 2 n-2 n−2的字符模式p的后 n − 2 n-2 n−2个字符匹配,此时还差最后一个字符,只需要比较s的最后一个字符与p的倒数第2个字符看看是否相等,不相等,后面还有一个字符,可以用来匹配吗?不一定,因为你还不知道p的最后一个字符之前m-1个的字符是否与s的前m-1个字符匹配,假如不匹配,走不通;假如匹配,那么情况就会转移到(1)中的情况,即: f ( m − 1 , n − 1 ) = 1 f(m-1,n-1)=1 f(m−1,n−1)=1
- f ( m − 1 , n − 3 ) = 1 f(m-1,n-3)=1 f(m−1,n−3)=1表示长度为 m − 1 m-1 m−1的字符串s与长度为 n − 3 n-3 n−3的字符模式p的后 n − 3 n-3 n−3个字符匹配,此时还差最后一个字符,只需要比较s的最后一个字符与p的倒数第3个字符看看是否相等,不相等,后面还有2个字符,可以用来匹配吗?不一定,因为你还不知道p的倒数第2个字符之前m-1个的字符是否与s的前m-1个字符匹配,假如不匹配,走不通;假如匹配,那么情况就会转移到(2)中的情况,即: f ( m − 1 , n − 2 ) = 1 f(m-1,n-2)=1 f(m−1,n−2)=1
- …剩下的情况不言自明了吧
于是可以给出这个问题的状态转移方程了:
f ( i , j ) = ( f ( i − 1 , j − 1 ) 与 操 作 s [ i ] = = p [ j ] ) ∣ ∣ ( f ( i − 1 , j − 2 ) 与 操 作 s [ i ] = = p [ j − 1 ] ) ∣ ∣ ( f ( i − 1 , j − 3 ) 与 操 作 s [ i ] = = p [ j − 2 ] ) . . . . . . . f(i,j) = (f(i-1,j-1)与操作s[i]==p[j])\ ||\ (f(i-1,j-2)与操作s[i]==p[j-1])\ ||\ (f(i-1,j-3)与操作s[i]==p[j-2])....... f(i,j)=(f(i−1,j−1)与操作s[i]==p[j]) ∣∣ (f(i−1,j−2)与操作s[i]==p[j−1]) ∣∣ (f(i−1,j−3)与操作s[i]==p[j−2]).......
因为采用的是int型存储,所以上述逻辑表达式也可以写成:
f ( i , j ) = ( f ( i − 1 , j − 1 ) ∗ s [ i ] = = p [ j ] ) + ( f ( i − 1 , j − 2 ) ∗ s [ i ] = = p [ j − 1 ] ) + ( f ( i − 1 , j − 3 ) ∗ s [ i ] = = p [ j − 2 ] ) . . . . . + ( f ( i − 1 , i − 1 ) ∗ s [ i ] = = p [ i ] ) f(i,j) = (f(i-1,j-1)*s[i]==p[j])\ +\ (f(i-1,j-2)*s[i]==p[j-1])\ +\ (f(i-1,j-3)*s[i]==p[j-2]).....\ +\ (f(i-1,i-1)*s[i]==p[i]) f(i,j)=(f(i−1,j−1)∗s[i]==p[j]) + (f(i−1,j−2)∗s[i]==p[j−1]) + (f(i−1,j−3)∗s[i]==p[j−2])..... + (f(i−1,i−1)∗s[i]==p[i])
最后只要 f ( m , k ) f(m,k) f(m,k)大于0就表示可以匹配,(这里 k = m , m + 1 , . . . n k=m,m+1,...n k=m,m+1,...n)
接来下的难点就是判断匹配的问题了:
即如何判断
s
[
i
]
=
=
p
[
j
]
s[i]==p[j]
s[i]==p[j],这个问题很麻烦,因为出现了字符*和字符.而且还需要建立一张二维映射表。
二维映射表:第一个维度,记录下当前位置,第二个维度,该位置下,字符*可以匹配的字符有哪些。可以匹配的字符有:a-z外加字符.,一共27个
实际上字符*有个短路效应,假设当前位置为j,之前出现过字符.,那么该字符模式p一定能与字符串s匹配
其原因是字符*可以替换成多个字符.,而字符.可以匹配任意字符,所以这个时候可以直接返回true
(二)正确的理解
1、动态规划
设 f ( i , j ) f(i,j) f(i,j)表示长度为 i i i的字符串s与长度为 j j j的字符模式p是否完全匹配
假设,输入的字符串s的长度为m,字符模式p的长度为n,则我们要求解的是
f
(
m
,
n
)
f(m,n)
f(m,n)
同样的,来看看
f
(
m
,
n
)
f(m,n)
f(m,n)是怎么得出来的:
因为题目要求的是完全匹配,也就是说 f ( m , n ) = = t r u e f(m,n)==true f(m,n)==true的前提是之前的字符串都匹配,此时无非有两种情况:
- 没有出现字符*的情况: f ( m − 1 , n − 1 ) = = t r u e f(m-1,n-1)==true f(m−1,n−1)==true,然后判断 s [ m ] = = p [ n ] s[m]==p[n] s[m]==p[n]
- 出现了字符*的情况。这种情况比较复杂,我会在后面进行讨论
出现字符*时,应该结合前一个字符char进行判断,其含义,根据需要可以将这两个字符视作:
(1)空字符串
(2)1个char字符
(3)2个char字符
…
(4)n个char字符
实际上我们需要对星花符出现的情况进行深入分析
假设 p [ j ] = ′ ∗ ′ p[j]='*' p[j]=′∗′,在计算 f ( i , j ) f(i,j) f(i,j)时:
(1)考虑替换为空字符串:
此时应该连带将
p
[
j
−
1
]
p[j-1]
p[j−1]一并消除,因此有:
f ( i , j ) = f ( i − 1 , j − 2 ) f(i,j)=f(i-1,j-2) f(i,j)=f(i−1,j−2)
(2)考虑替换为
n
n
n个字符
p
[
j
−
1
]
p[j-1]
p[j−1]
这里n的取值为1~inf
按照一般的想法,如果替换1个以上的字符时,应该是要考虑一下 s [ i + 1 ] s[i+1] s[i+1]或者是看看 p [ j + 1 ] p[j+1] p[j+1]再进行决定。
但是这与动态规划的原理不符,动态规划研究的是:当前状态与之前状态的关系
采用上面的想法,就变成了:当前状态与之后状态的关系
所以,我们假设字符串被截断了,就截断在 p [ j ] p[j] p[j]与 s [ i ] s[i] s[i],按照动态规划的原则,不再考虑之后的情况,怎么做呢?
既然 p [ j − 1 ] p[j-1] p[j−1]与 p [ j ] p[j] p[j]作为复合字符,能够视作多个字符,那么是不是可以假设: f ( i − 1 , j ) = t r u e f(i-1,j)=true f(i−1,j)=true呢?
如果这个假设成立的话,是不是还可以继续往前推呢?
比如: f ( i − 2 , j ) = t r u e f(i-2,j)=true f(i−2,j)=true, f ( i − 3 , j ) = t r u e f(i-3,j)=true f(i−3,j)=true…乃至 f ( 1 , j ) = t r u e f(1,j)=true f(1,j)=true
举个例子:
s="aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
p="aaa*aa"
这里j=4,很明显:对于i>3时,有:f(i,4)=true
如果已知:f(5,4)=true
我们该如何推出f(6,4)呢?
首先f(5,4)成立是f(6,4)成立的基础
这里其实是:保持字符串p的长度不变而增加字符串s的长度
f(6,4)=f(5,4)&&(p[3]==s[6]||p[3]=='.')
那你可能会问了能不能:保持字符串s的长度不变而增加字符串p的长度
这么做毫无意义,因为这是根据字符串p的等价长度是在其基础长度上可以增加的
所以我们才会尝试着增加字符串s的比较长度
通过上面的例子,我们可以发现:
f ( i , j ) = f ( i − 1 , j ) & & ( p [ j − 1 ] = = s [ i ] ∣ ∣ p [ j − 1 ] = = ′ . ′ ) f(i,j)=f(i-1,j)\&\&(p[j-1]==s[i]||p[j-1]=='.') f(i,j)=f(i−1,j)&&(p[j−1]==s[i]∣∣p[j−1]==′.′)
注意:C++里面用string存储,起始位置是0,不是1,长度为 j j j的字符串s,第 j j j个字符是 s [ j − 1 ] s[j-1] s[j−1]
C++代码:
class Solution {
public:
bool isMatch(string s, string p) {
// match_tabel[i][j]=true 表示s字符串的前i个字符与字符串p的前j个字符串是匹配的
vector<vector<bool>> match_tabel(s.size() + 1, vector<bool>(p.size() + 1));
// 将字符串的首位填充掉,为了与match_tabel里面的变量进行统一
// 初始化条件
match_tabel[0][0] = true; // 空字符的情况,只有两个字符串都为空
match_tabel[0][1] = false; // 1个字符是不可能与空字符串匹配的
// 空字符串只能与char*匹配,因此必须两个字符两个字符的进行匹配
// 因此match_tabel[0][j]考虑的是:空字符串 与 p[j-2]p[j-1] 的匹配
for (int j = 2; j <= p.size(); j++)
match_tabel[0][j] = match_tabel[0][j - 2] && p[j - 1] == '*';
// match_tabel:里面的i,j表示的是字符串的长度
// 字符串是0开始存储的,所以和长度差个1
for (int i = 1; i <= s.size(); i++)
for (int j = 1; j <= p.size(); j++) {
if (p[j - 1] != '*')
match_tabel[i][j] = match_tabel[i - 1][j - 1] && (p[j - 1] == s[i - 1] || p[j - 1] == '.');
else
match_tabel[i][j] = (j > 1) && (match_tabel[i][j - 2] || (match_tabel[i - 1][j] && (p[j - 2] == s[i - 1] || p[j - 2] == '.')));
}
return match_tabel[s.size()][p.size()];
}
};
执行效率: