c语言相关性匹配_LeetCode第44题: 通配符匹配isMatch(C语言)

引子

思路:看到两个序列去匹配的问题,最自然的想法是双层循环尝试对齐匹配,我们假设表格数字为1代表匹配成功,0代表匹配失败。

图1

分析:分别遍历s和p两个字符串,如果p[i] == s[j],则表示匹配成功,否则直接退出循环遍历。

用代码也很好实现

bool isMatch(char * s, char * p){

int n = strlen(s);

int m = strlen(p);

int dp[m][n];

for(int i = 0; i < m; I++)

for(int j = 0; j < n; j++)

dp[i][j] = 0;

for (int i = 0; i < m; i++){

for (int j = 0; j < n; j++){

if (s[j] == p[I])

dp[i][j] = 1;

else

return false;

}

}

return dp[m][n];

}

但是,事实并不会这样简单,因为有特殊符号星号('*')和问号('?')的加入,让事情变得复杂。细致分析的话可能情况很多已经无从下手了。

1、s 和 p 都为空

既然无从下手,我们就用最笨的办法,从最简单的方法逐步去逼近自然的情况,自然,最简单的情况当然是 p 和 s 均为空了。

图2

分析:两个都为空,是匹配成功的。

结论:匹配成功

2、p为空

假设 p = '', s = 'abcd',将情况1中融合进去

图3

分析:当 p 为空时,

1、如果 s 为空的时候,匹配成功

2、如果 s 非空的时候,匹配失败

结论:匹配失败

3、s为空

如果拿上例来类推的话,会理所当然的认为:

当 s 为空时,

1、如果 p 为空的时候,匹配成功

2、如果 p 非空的时候,匹配失败

对于情况1,当然没有疑问了;对于情况2,如果没有星号('*')、问号('?')特殊符号捣乱的话,确实也会匹配失败,但如果考虑星号('*')可以匹配空的情况的时候,如下图:

3.1 p = '*'

同样将情况1融合进来

图4

结论:匹配成功

3.2、p为'**' 时

图5

结论:匹配成功

也就是说,当 s 为空时,

1、如果 p 为空或者全部由星号('*')组成,匹配成功;

2、p 除了星号('*')之外还有其他的字符或者问号('?')的时候,还是需要分析一下:

3.3、 p包含星号('*')外的其他字符

3.3.1、p = '**a' 时

图6

分析:星号('*')可以匹配空字符没有错,所以dp[0]、dp[1]都可以表示匹配成功,这个在上例中已经分析过了,可是到匹配p[2]的时候,匹配失败

结论:匹配失败

我们再调整一下星号('*')与'a'出现的先后次序

3.3.2、p = '*a*' 时

图7

分析:同样的,dp[0] = 1,可是dp[1] = a,依然无法匹配空字符,所以dp[1] = 0,这里的区别在于虽然p[2] = '*',可是dp[2]无法延续上述的情况,因为中间被p[1]隔断了,所以这里的dp[2] = 0。如果从物理意义上解释的话,p = '*a*'应该去匹配一个字符串,字符串的格式为中间含有一个字符'a',这个字符'a'的左右两边包含任意多个由空格和任意字母组成的序列,比如 s = 'cd ef a we',但是假如s 为空,那肯定是匹配不上的。

结论:匹配失败

再调换一下星号('*')与'a'出现的先后次序

3.3.3、 p = 'a**' 时

图8

分析:同上例一样, p = 'a**' 是用来匹配以字符'a'开头的,后面跟着包含任意多个由空格和任意字母组成的序列,比如 s = 'ad fwe',而s 为空的情况,也是匹配不上的。

结论:匹配失败

3.3.4、 p 只包含星号('*')和 '?'

同样的分析可以将上述的字符'a'换成'?',结论也是一样的

图9

分析:我们在总结星号('*')与字母出现的顺序可以发现:只有从第一位开始连续出现星号('*')的时候,才会连续的出现1,否则就意味着1的连续性被打断,后续均为0。知道这一点,对将来的一般性模式拓展及总结很有用,我们甚至可以用代码来实现在s为空p非空情况下,匹配结果究竟是0或者1的规律。很明显p有下图三种情况,而且,这个情况很简单,我们可以直接画出结果。

总结,当 s 为空时,

1、如果 p 为空或者全部由星号('*')组成,匹配成功;

2、p 除了星号('*')之外还有其他的字符或者问号('?')的时候,匹配失败,失败点在p第一次其他字母'a-z'或者问号('?'),接下来无论出现任何字符,如果p的第一个字符不是星号('*'),则从第一个位置开始均匹配失败。

图10

如果要将3.3节的所有情况用代码来实现的话,假设 p 的长度为m时,定义一个数组 dp[m + 1],用来记录 p 与空字符串 s 匹配情况,其中的dp[0] = 1,代表 p 也为空的时候,p与s匹配成功的状态,同时也将dp[0] = 1作为我们的初始状态。

int m = strlen(p);

int dp[m + 1];

dp[0] = 1;

for (int i = 1; i <= m; I++)

dp[i] = dp[i - 1] && p[i - 1] == '*';

4、s p 均不为空

有了上面的铺垫,我们来总结最后一种情况。其实,对于s、p均不为空的情况(假设s长度为n,p长度为m),可以将以上所述的情况整合在一起,整合的方法就是新初始化一个数组dp[m + 1][n + 1],其中的第0行和地0列的值其实就是上述所有情况的整合,而且,第0行和第0列的所有数字,可以直接算出来。我们依然从最简单的情况开始:

4.1、s = 'a',p = 'b'

图11

分析:因为p[0] != s[0], 所以dp[1][1] = 0

结论:匹配失败

4.2、p = 'a' ,s = 'a'

图12

分析:因为p[0] == s[0], 所以dp[1][1] = 1

结论:匹配成功

4.3、p = '*' ,s = 'a'

图13

分析:因为p[0] = '*' , 可以匹配任意字符,所以无论s[0]为任何字母均可以匹配, 所以dp[1][1] = 1

结论:匹配成功

4.4、p = '?' ,s = 'a'

图14

分析:因为p[0] = '?' , 可以匹配任意非空字符,所以无论s[0]为任何字母均可以匹配, 所以dp[1][1] = 1

结论:匹配成功

以上四种情况,是最简单的情况,我们当然可以通过四个if条件判断就可以得出结果,我们接下来再升级一下难度,看看能不能总结出一个规律,可以覆盖最一般的情况。

4.5、p = 'ab' ,s = 'ab'

dp的初始化结果并结合4.2分析的dp[1][1] = 1的结论可知:

图15

分析:本例其实就是引子的简化版,但也并没有什么不同。现在还剩下三个空格需要我们补充,针对本案例,我们只希望知道dp[2][2]的值,因为p[1] == s[1] == 'b' , 所以dp[2][2] = 1。

结论:匹配成功

现在问题来了,如果我们每次遇到p[i] == s[j], 就设定dp[i][j] = 1,这个是不是我们要找的规律呢?

答案当然是不能,请看下例

4.6、p = 'cb' ,s = 'ab'

dp的初始化结果并结合4.1分析的dp[1][1] = 0的结论可知:

图16

分析:我们直接来看dp[2][2],我们知道,我们定义的dp数组,就是来记录s和p对应位置字符的匹配情况的,针对本例,很明确,是不匹配的,虽然p[1] == s[1],但是p[0] != s[0],所以dp[2][2] 和 dp[1][1] 均等于0。

结论:匹配失败

规律一:

当p[i] == s[j] 时判定dp[i + 1][j + 1]的值时

1、如果dp[i][j] == 0, 则dp[i + 1][j + 1] = 0,表达的意思是如果s和p前一个对应位置匹配失败,当前位置的字母也跟着匹配失败

2、如果dp[i][j] == 1, 则dp[i + 1][j + 1] = 1,表达的意思是如果s和p前一个对应位置匹配成功,当前位置的字母也跟着匹配成功

这里需要解释一下的是,因为我们分别在s和p之前多加了一行和一列的用于概括s或者p为空的情况,所以当记录p[i] 和 s[j]匹配的时候,dp相应p[i] 和 s[j]的位置不是dp[i][j] 而是dp[i + 1][j + 1],dp[i][j]对应的记录的是p[i - 1] 和 s[j - 1]

上面的规律也很容易用代码实现

if(p[i] == s[j]){

if(dp[i][j] == 1)

dp[i + 1][j + 1] = 1;

else

dp[i + 1][j + 1] = 0;

}

简化一下也就是这样

if(p[i] == s[j])

dp[i + 1][j + 1] = dp[i][j];

4.7、 p = 'a?c' ,s = 'abc'

dp的初始化结果并结合4.2分析的dp[1][1] = 0的结论可知:

图17

分析:因为问号('?')可以匹配任何非空的字母, 不难得出,当p[i] == '?'时,不管s[j]为任何非空的字母,其实都可以直接认为p[i] == s[j] ,对dp的处理都可以沿用规律一的总结,我们就暂且称这样的发现为规律二吧。

结论:匹配成功

规律二:

当p[i] == '?' 时判定dp[i + 1][j + 1]的值时

1、如果dp[i][j] == 0, 则dp[i + 1][j + 1] = 0,表达的意思是如果s和p前一个对应位置匹配失败,当前位置的字母也跟着匹配失败

2、如果dp[i][j] == 1, 则dp[i + 1][j + 1] = 1,表达的意思是如果s和p前一个对应位置匹配成功,当前位置的字母也跟着匹配成功

规律二也很容易用代码表示出来

if(p[i] == '?')

dp[i + 1][j + 1] = dp[i][j];

将规律一和规律二合并之后就变成

if(p[i] == s[j] || p[i] == '?')

dp[i + 1][j + 1] = dp[i][j];

4.8、s = 'abc', p = 'a*c'

dp的初始化结果并结合4.2分析的dp[1][1] = 1的结论可知:

图18

分析:dp[1][1] = 1的结论我们已经反复看了几遍了,下面我们来确定dp[2][2]的值,如果参考规律一和规律二的结论,星号('*')可以匹配任何的字母,针对本例中dp[1][1] = 1的前提,dp[2][2]也应该等于1,同理dp[3][3] = 1,完美匹配。

结论:我们人眼肯定是能看出是匹配的

上面的分析,可能会让人误认为星号('*')没有特殊的地方?错!

我们继续往下看。

4.9、s = 'dec', p = 'a*c'

dp的初始化结果并结合4.2分析的dp[1][1] = 0的结论可知:

图19

分析:如果参考规律一的结论,dp[1][1] = 0,那么dp[2][2] 也等于0吗?是的,dp[2][2] = 0,那么dp[3][3] 呢?看到p[2] == s[2] == 'c',同样根据规律一递推dp[3][3] = 0,事实上dp[3][3] 也确实应该等于 0。别着急去翻4.8,再耐心往下看两步。

结论:人眼知道是匹配失败的,但是如何让机器代码来计算还需要进一步的分析

4.10、s = 'ac', p = 'a*c'

dp的初始化结果并结合4.8分析的结论可知:

图20

分析:我们先说结论吧,s = 'ac', p = 'a*c' 是可以匹配成功的,p[0] 匹配 s[0], p[1] 匹配空,p[2] 匹配 s[2]。可是当我们用代码来推导dp[3][2]的时候,是应该依据dp[2][2]的值吗?

结论:还是往下看吧

现在,是时候来总结一下星号('*')的真正规律了,在之前的所有讨论中,我们只关心dp数组对角的元素,原因也是因为,我们大脑更愿意先看到最理想或者最容易思考的情况,也就是s 和 p长度相等的情况,其实我们也应该关注对角线外的元素,因为题目的设计星号('*')的可以匹配任意字母甚至包括非空字符,所以这就会导致即使p和s不等长,依然可能匹配成功。

敲黑板!星号规律('*')总结开始了

星号('*')的出现把问题的复杂度升级了许多,开始短路了。

我们再来重申一下星号('*')的定义:可以匹配任意字符串(包括空字符串)。

虽然星号('*')看似很强大,但是也要考虑束缚条件,比如在例4.9中,s = 'dec', p = 'a*c' ,p[0] != s[0],当要用p[1]也就是星号('*')来做匹配的时候,自然是要考虑dp[1][1]的值。再看一个星号('*')匹配空字符的情况,巩固一下p和s不等长,依然可能匹配成功的例子。

4.11、s = 'abdc', p = 'a*c'

dp的初始化结果并结合4.8分析的结论可知:

图21

分析:依然先说结论,本例中的 s 和 p 确实是匹配成功的,p[0] 匹配 s[0], p[1] 同时匹配 s[1] 和 s[2],p[2] 匹配 s[3]。但是我们dp[3][3]的时候,由于p[2] = 'c', s[2] = 'd',那么dp[3][3] = 0,本例特殊的点在于 m != n,我们最终是要求d[m][n],对于本例则是要求出d[3][4],可是d[3][4]怎么求呢,比照规律一,我们要知道他的上一个的匹配结果,也就是需要知道d[2][3],物理上的解释就是首先要判断它的前一个字符对是否能匹配成功。看来我们真的需要关注dp数组的所有元素的值了。

对于第一行的值,也就是用p[0]来匹配 s 各个位置的值,p[0] = a,很自然,当遍历s的时候,遇到s[i] == 'a',则dp[1][i + 1] = 1,否则dp[1][i + 1] = 0。补全之后如下图所示:

图22

但是对于第二行的值就难办了,因为p[1] = '*',所以当p[1] 与 s[0] = 'a'匹配的时候应该等于1呢还是应该等于0呢?也许有人要说了当然等于1了,因为星号('*')的定义:可以匹配任意字符串(包括空字符串)。如果真的是这样的话,那么是不是说只要星号('*')所在的行,都应该为1?

大写的STOP

我们先跳回例4.9,按照上述方法,我们也很容易补全第一行和“第二行”如下图所示:

图23

细心的同学发现了,我们补全第一行是没有问题的,第一行的数据值全部为0,但是我们把第二行加了双引号,权当假设之前的结论是准确的,那我们是不是可以继续把第三行补全呢?

图24

震惊!dp[3][4] = 0

可是明明dp[3][3] = 1!

这下,我们是不是已经猜到了,虽然星号('*')可以匹配任意字符,但是武断的把星号('*')根据规律一来判断,应该是不对的。

我们把4.9 和4.11的图合并在一起看吧

图25

两个图合并在一起之后,我们同步来看p[1] = '*',而且我们专注于求例dp[2][1],对于左右两个图,他们唯一不同的点在于dp[1][1]的值,左图的dp[1][1] = 0,而右侧的dp[1][1] = 1,原因也是因为左图的p[0] 和 s[0]确实没有匹配成功,而右图的p[0] 和 s[0]匹配成功了。

从物理意义上理解的话,当遇到星号('*')的时候:

1、如果星号('*')前面的元素(针对本例星号('*')前面的元素是'a')匹配成功(比如右图的情况),那么星号('*')所在的对应位置(本例中的dp[2][1])也可以假设是匹配成功,即右图dp[2][1] = 1;

2、如果星号('*')前面的元素(针对本例星号('*')前面的元素是'a')(比如左图的情况)那么星号('*')所在的对应位置(本例中的dp[2][1])也可以假设是匹配失败,即左图dp[2][1] = 0。

再把图更新一下啊

图26

这样是不是说:

如果p[i] == '*',当计算dp[i + 1][j + 1]的值时,只需要去查看dp[i][j + 1]。用代码表示就是

if (p[i] == '*')

dp[i + 1][j + 1] = dp[i][j + 1];

先把假设再完善一下

图27

应该已经发现不对了,对于左图,在星号('*')所在的行全部置0其实没问题,因为左图本身确实匹配失败了,可是右图呢,我们本来就知道右图是匹配成功了呀,我们把回去看 图4.9和4.11合并图_b ,我们现在重点关注一下左右两图dp[2][2]的位置

先说相同点:两个图中dp[2][2]正上方也就是dp[1][2]的位置均为0

再说不同点:

左图中,dp[2][2]的左方dp[2][1]和左上方dp[1][1]均为0;

右图中,dp[2][2]的左方dp[12][1]和左上方dp[1][1]均为1。

会不会有疑问,星号('*')左方和左上方的位置一定相等呢?或者说当遇到星号('*')的时候,是不是还要考虑星号('*')左方和左上方的位置的值呢?因为这两个位置确实会现实星号('*')之前的匹配情况啊。针对左右两图中的dp[2][2],其实他们左方dp[2][1]本身就是由左上方dp[1][1]值决定的,所以左方dp[2][1]已经反应了左上方dp[1][1]的匹配情况了。

我们先把dp[2][2]的值究竟是1 还是0 先搁置下来,我们来看看dp[2][2]右侧的dp[2][3]的值

先说相同点:两个图中dp[2][3]正上方也就是dp[1][3]的位置均为0,左上方也就是位置dp[1][2]均为0,dp[2][3]的左方dp[2][2]的值还不好确定

再说不同点:

完了,没有不同点,唯一的不同点就是左右两图后面的列数不一致。

可是实际上呢,我们应该隐隐约约可以看到,这两个位置的值应该也是不同的。可是这个不同怎么递推出来呢?依然是隐隐约约可以察觉,对于位置dp[2][3],其实左侧dp[2][2]的值可以反应出其之前的匹配情况的,同样的,dp[2][2]也是可以通过dp[2][1]来反应区别不同。

现在我们先把dp[2][2]的值分别填上

图28

也就是说,

如果p[i] == '*',当计算dp[i + 1][j + 1]的值时,不仅需要去查看dp[i][j + 1],也需要看dp[i + 1][j]。经过上述一系列的分析,只需要

dp[i][j + 1] 或者dp[i + 1][j]其中一个为1,那么dp[i + 1][j + 1] 就等于 1。用代码表示如下:

if (p[i] == '*')

dp[i + 1][j + 1] = dp[i][j + 1] || dp[i + 1][j];

可是上面只是我们的推测,事实真的是这样吗?我们再把最常规的情况摆出来

4.12、当 p = '*a*b',s = 'adceb' 时的初始化结果:

图29

综合以上的分析,全部填满之后

图30

4.13、p = 'ab',s = 'adceb'

dp的初始化结果并结合4.8分析的结论可知:

图31

图32

至此,所有的分析都结束了,我们开始正式设计代码。

1、首先要获取p 和 s 的长度,假设分别为m、n;

2、声明一个数组dp[m + 1][n + 1],并对其第零行和第零列进行初始化;

3、分别对p 和 s进行遍历,将dp数组的每个位置进行赋值

有三种情况:

3.1、 如果s[i]==p[j] or p[j]=='?' 则 dp[i+1][j+1] = dp[i][j]

3.2、 如果p[j]=='*' 则dp[i+1][j+1] = dp[i][j+1] || dp[i+1][j]

3.3、 s[i] != p[j] 则 dp[i+1][j+1]=0;

4、返回结果即dp[m][n]即可

其实,这个图就是将图3.6和图2整合了一下而已。

好了,到了这一步,我们就可以来分析计算剩下来的所有空格应该对应的值了,也就是从这一步开始,我们要分析动态规划里最难找的最优子状态了。

bool isMatch(char * s, char * p){

int n = strlen(s);

int m = strlen(p);

bool dp[m + 1][n + 1];

for (int i = 0; i < m + 1; i++)

for(int j = 0; j < n + 1; j++)

dp[i][j] = 0;

dp[0][0] = true;

for (int i = 1; i <= m; i++)

dp[i][0] = dp[i - 1][0] && p[i - 1] == '*';

for (int i = 0; i < m; i++){

for (int j = 0; j < n; j++){

if (s[j] == p[i] || p[i] == '?'){

dp[i + 1][j + 1] = dp[i][j];

}

else if (p[i] == '*'){

dp[i + 1][j + 1] = dp[i][j + 1] || dp[i + 1][j];

}

else{

dp[i + 1][j + 1] = 0;

}

}

}

return dp[m][n];

}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
KMP算法是一种字符串匹配算法,用于在一个文本串S内查找一个模式串P的出现位置。它的时间复杂度为O(n+m),其中n为文本串的长度,m为模式串的长度。 KMP算法的核心思想是利用已知信息来避免不必要的字符比较。具体来说,它维护一个next数组,其中next[i]表示当第i个字符匹配失败时,下一次匹配应该从模式串的第next[i]个字符开始。 我们可以通过一个简单的例子来理解KMP算法的思想。假设文本串为S="ababababca",模式串为P="abababca",我们想要在S中查找P的出现位置。 首先,我们可以将P的每个前缀和后缀进行比较,得到next数组: | i | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | | --- | - | - | - | - | - | - | - | - | | P | a | b | a | b | a | b | c | a | | next| 0 | 0 | 1 | 2 | 3 | 4 | 0 | 1 | 接下来,我们从S的第一个字符开始匹配P。当S的第七个字符和P的第七个字符匹配失败时,我们可以利用next[6]=4,将P向右移动4个字符,使得P的第五个字符与S的第七个字符对齐。此时,我们可以发现P的前五个字符和S的前五个字符已经匹配成功了。因此,我们可以继续从S的第六个字符开始匹配P。 当S的第十个字符和P的第八个字符匹配失败时,我们可以利用next[7]=1,将P向右移动一个字符,使得P的第一个字符和S的第十个字符对齐。此时,我们可以发现P的前一个字符和S的第十个字符已经匹配成功了。因此,我们可以继续从S的第十一个字符开始匹配P。 最终,我们可以发现P出现在S的第二个位置。 下面是KMP算法的C++代码实现:

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值