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];
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,294评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,493评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,790评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,595评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,718评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,906评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,053评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,797评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,250评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,570评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,711评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,388评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,018评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,796评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,023评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,461评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,595评论 2 350