LeetCode10-正则表达式匹配

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[i1]字符

也就是只能换成前一个位置的字符

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,j1)+(f(i1,j1)s[i]==p[k1])+(f(i1,j2)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[ji+1,...,j]

最终我们要知道是长度为m的字符串s与长度为n的字符模式p是否匹配,如果是匹配的,它是怎么得出来的?

考虑最后一步的情况:

  • f ( m − 1 , n − 1 ) = 1 f(m-1,n-1)=1 f(m1,n1)=1表示长度为 m − 1 m-1 m1的字符串s与长度为 n − 1 n-1 n1的字符模式p的后 m − 1 m-1 m1个字符匹配,此时还差最后一个字符,只需要比较最后一个字符看看是否相等,不相等则不匹配,这条路走不通了
    你可能会说会说,
  • f ( m − 1 , n − 2 ) = 1 f(m-1,n-2)=1 f(m1,n2)=1表示长度为 m − 1 m-1 m1的字符串s与长度为 n − 2 n-2 n2的字符模式p的后 n − 2 n-2 n2个字符匹配,此时还差最后一个字符,只需要比较s的最后一个字符与p的倒数第2个字符看看是否相等,不相等,后面还有一个字符,可以用来匹配吗?不一定,因为你还不知道p的最后一个字符之前m-1个的字符是否与s的前m-1个字符匹配,假如不匹配,走不通;假如匹配,那么情况就会转移到(1)中的情况,即: f ( m − 1 , n − 1 ) = 1 f(m-1,n-1)=1 f(m1,n1)=1
  • f ( m − 1 , n − 3 ) = 1 f(m-1,n-3)=1 f(m1,n3)=1表示长度为 m − 1 m-1 m1的字符串s与长度为 n − 3 n-3 n3的字符模式p的后 n − 3 n-3 n3个字符匹配,此时还差最后一个字符,只需要比较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(m1,n2)=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(i1,j1)s[i]==p[j])  (f(i1,j2)s[i]==p[j1])  (f(i1,j3)s[i]==p[j2]).......

因为采用的是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(i1,j1)s[i]==p[j]) + (f(i1,j2)s[i]==p[j1]) + (f(i1,j3)s[i]==p[j2])..... + (f(i1,i1)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(m1,n1)==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[j1]一并消除,因此有:

f ( i , j ) = f ( i − 1 , j − 2 ) f(i,j)=f(i-1,j-2) f(i,j)=f(i1,j2)

(2)考虑替换为 n n n个字符 p [ j − 1 ] p[j-1] p[j1]
这里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[j1] p [ j ] p[j] p[j]作为复合字符,能够视作多个字符,那么是不是可以假设: f ( i − 1 , j ) = t r u e f(i-1,j)=true f(i1,j)=true呢?

如果这个假设成立的话,是不是还可以继续往前推呢?

比如: f ( i − 2 , j ) = t r u e f(i-2,j)=true f(i2,j)=true f ( i − 3 , j ) = t r u e f(i-3,j)=true f(i3,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(i1,j)&&(p[j1]==s[i]p[j1]==.)

注意:C++里面用string存储,起始位置是0,不是1,长度为 j j j的字符串s,第 j j j个字符是 s [ j − 1 ] s[j-1] s[j1]

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()];
	}
};

执行效率:
在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值