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)表示长度为iii的字符串s与长度为jjj的字符模式p是否匹配
这句话有几个隐藏的条件:
(1)若f(i,j)==1f(i,j)==1f(i,j)==1,则j>=ij>=ij>=i,且对于任意的k>=jk>=jk>=j,都有f(i,k)==1f(i,k)==1f(i,k)==1
(2)f(0,j)==1f(0,j)==1f(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[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])........)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]应该与ppp中的哪个位置的字符进行匹配,例如:
p=“abcabcabcabcabcabcabcd”
s=“abcd”
在这个例子中f(3,j)==1f(3,j)==1f(3,j)==1对任意的j>=3j>=3j>=3都成立,然而你却很难进行下一步的匹配计算,例如计算f(4,7)f(4,7)f(4,7)。
我们来算算这个结果,因为f(3,7)==1f(3,7)==1f(3,7)==1而且f(3,6)==1f(3,6)==1f(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)表示长度为iii的字符串s与长度为jjj的字符模式p的后iii个字符是否匹配
也就是说:f(i,j)==1f(i,j)==1f(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)=1f(m-1,n-1)=1f(m−1,n−1)=1表示长度为m−1m-1m−1的字符串s与长度为n−1n-1n−1的字符模式p的后m−1m-1m−1个字符匹配,此时还差最后一个字符,只需要比较最后一个字符看看是否相等,不相等则不匹配,这条路走不通了
你可能会说会说, - f(m−1,n−2)=1f(m-1,n-2)=1f(m−1,n−2)=1表示长度为m−1m-1m−1的字符串s与长度为n−2n-2n−2的字符模式p的后n−2n-2n−2个字符匹配,此时还差最后一个字符,只需要比较s的最后一个字符与p的倒数第2个字符看看是否相等,不相等,后面还有一个字符,可以用来匹配吗?不一定,因为你还不知道p的最后一个字符之前m-1个的字符是否与s的前m-1个字符匹配,假如不匹配,走不通;假如匹配,那么情况就会转移到(1)中的情况,即:f(m−1,n−1)=1f(m-1,n-1)=1f(m−1,n−1)=1
- f(m−1,n−3)=1f(m-1,n-3)=1f(m−1,n−3)=1表示长度为m−1m-1m−1的字符串s与长度为n−3n-3n−3的字符模式p的后n−3n-3n−3个字符匹配,此时还差最后一个字符,只需要比较s的最后一个字符与p的倒数第3个字符看看是否相等,不相等,后面还有2个字符,可以用来匹配吗?不一定,因为你还不知道p的倒数第2个字符之前m-1个的字符是否与s的前m-1个字符匹配,假如不匹配,走不通;假如匹配,那么情况就会转移到(2)中的情况,即:f(m−1,n−2)=1f(m-1,n-2)=1f(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,...nk=m,m+1,...nk=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)表示长度为iii的字符串s与长度为jjj的字符模式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)==truef(m,n)==truef(m,n)==true的前提是之前的字符串都匹配,此时无非有两种情况:
- 没有出现字符*的情况:f(m−1,n−1)==truef(m-1,n-1)==truef(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)考虑替换为nnn个字符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)=truef(i-1,j)=truef(i−1,j)=true呢?
如果这个假设成立的话,是不是还可以继续往前推呢?
比如:f(i−2,j)=truef(i-2,j)=truef(i−2,j)=true,f(i−3,j)=truef(i-3,j)=truef(i−3,j)=true…乃至f(1,j)=truef(1,j)=truef(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,长度为jjj的字符串s,第jjj个字符是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()];
}
};
执行效率:


本文详细解析了LeetCode10题——正则表达式匹配问题的动态规划解法,包括如何理解题目要求及核心算法设计。
1157

被折叠的 条评论
为什么被折叠?



