判断字符串str和pattern是否匹配?

判断字符串str和pattern是否匹配?

提示:这道题非常困难,但是也要分清楚细节,搞清楚暴力递归的定义,摸清如何修改动态规划代码


题目

字符串str只包含a–z的小写字母,且0<=str.len<=100,pattern除了含有小写字母之外,还有.或x*,【x是任意a–z】。
.可以变为单个字符(a–z任意变);
x*可以变为0–N个字符(无限制长度的);

请问字符串str能否与pattern匹配?


一、审题

换句话说:
pattern,能否通过变换.和x*,变为str?是true,否则false

首先我们来看
.必须变一个单字符,a–z,不能变没了;所以有.的单独出现,就必须有一个字符变出来;
比如:
str=abcde,pattern=abc.e=abcde

x星 遇到了,则显然不能是**,这是不合法的串
x星 遇到了,可以是 . 星 ,比如str=abcde,pattern= . 星 = abcde
x星 可以变没了,也可以变很多不同的字符出来。
比如str=abcde,pattern= ab . 星 cde = abcde
str=abcde,pattern= abcde = abcde,只变出1个a来
str=aaabcde,pattern= a
abcde = aaabcde,变出2个a来
等等


二、解题

遇到这个题呢,需要熟悉它,然后写出暴力递归的代码,再转化为DP填表的动态规划代码

首先,咱们得检查str和pattern的合法性,因为:
str中不许有.和*
其次,pattern中,不许*做开头,因为必须出现x星配合使用;
另外,**不能同时出现,否则违规,必须是x星;

//检查字符串的合法性,str必须没有*与.,p才能有,但是p不能出现**
    public static boolean isValid(char[] s, char[] p){
        for (int i = 0; i < s.length; i++) {
            if (s[i] == '.' || s[i] == '*') return false;
        }
        for (int i = 0; i < p.length; i++) {
            if (p[i] == '*' && (i == 0 || p[i - 1] == '*')) return false;//*在开头或者**出现都不行
            //一旦出现在开头,直接返回false
            //不是开头才i-1查一下是否为*
        }

        //都查了合法
        return true;
    }

下面开始尝试暴力递归,既然是2个字符串,往往是一个样本做行,一个样本做列的样本位置对应模型;讨论某个位置i做开头,或者i做结尾时的情况;
以往可能经常会尝试pattern的0–j上可以变为str的0–i吗?
但是仔细一想,咱们经常会遇到i位置是x*
也就是说可能pattern的j位置是x,我们需要看j+1位置是不是*,所以讨论0–j是不合理的,我们需要讨论j–N-1位置!!!

因此定义这么一个暴力递归的函数:f(str,i,pattern,j)
str长为N,pattern长为M
f定义为pattern的j–M-1范围内的字符串,是否可以变为str的i–N-1范围上的字符串呢
有了这样的f=process定义,主函数这么调用:

//主函数
    public static boolean isMatch(String s, String p){
        if (s == null || p == null) return false;
        char[] str = s.toCharArray();
        char[] ptr = p.toCharArray();

        return isValid(str, ptr) && process(str, ptr, 0, 0);//从0位置开始匹配能匹配上吗,合法的情况下
    }

process函数如何写:
自然要分各种情况讨论:
(1)当j=M时,意味着pattern的M–M是一个空串""
此时,要行让"“空串变为str的i–N-1的话,必然也只能变为”“空串了,也就是说str的i–N-1=”",即i==N呗。
(2)来到j位置,如果j+1=M-1,pattern[j+1] 不是星,看j+1处pattern的字符啥状况:
故j+1处,可能是普通的字符,也可以是.
既然pattern[j+1]它不是星,而且是最后一个字符,那么str[i]位置必然不能是N位置(即i != N),至少也是N-1位置,即保证str[i]有一个字符,
且:想让pattern变成str,那得保证(str[i]=pattern[j] 或者 pattern[j]=.)
且保证f(i+1,j+1)是true;
图1
(3)来到j位置,pattern[j+1 ]是星呢?
这里已经潜在有一个条件,如果j+1是星,那么j处一定不是星,之前已经约定好了

——如果str[i]=pattern[j],或者pattern[j]=.
那类似于x星或者.星,这俩可以变为1–无穷个str[i–N-1]
比如:.星能变出0个a,1个a,也可以变出2个a,甚至更多的a来,这都是合理的
那就只需要再看看f(i,j+2),f(i+1,j+2),f(i+2,j+2),……f(i+无穷,j+2)
这啥意思?就是让pattern[j+2–M-1]那些字符,去匹配str[i–N-1],str[i+1–N-1],str[i+2–N-1],……str[N-1–N-1]的字符串
意味着,我们用.星,或者x星,变成了012–无穷个str中间那些字符【这不就是x星的规则吗,很容易理解】
图2
——注意,万一pattern[j]!=str[i]呢,那你x星就只能变出0个a来,直接看f(i,j+2)就行,啥意思?就是让pattern[j+2–M-1]那些字符,去匹配str[i–N-1]字符,让pattern中j,j+1对应的x星直接变没了。否则对不上str的。

这个代码,要仔细领略:

//暴力递归
    //str从i位置到结尾,ptr从j位置到结尾,能匹配上吗,也就是p能变成s吗
    public static boolean process(char[] str, char[] ptr, int i, int j){
        //当两者同时到结尾,可以认为没问题
        if (j == ptr.length) return i == str.length;

        //然后分两种情况,一个是j+1位置非*
        //既然j+1就不是*,那j位置必须要和i匹配,要么是相等,要么是.,否则配不上,当然保证i还有东西才行,且,后续位置也能配上才可以哦
        if (j + 1 == ptr.length || ptr[j + 1] != '*') return i != str.length &&//s不能提前耗尽了
                (ptr[j] == '.' || ptr[j] == str[i]) &&//相等
                process(str, ptr, i + 1, j + 1);//后续

        //然后是第二种情况,一个是j+1位置恰好就是*
        //1)加入ptrj!=stri,这个时候,j*必须变为0个i字符才行
        //2)当ptrj==stri的话,有很多种无限情况,j*可能变为0个字符,1个,2个,……无穷个,直到你前缀不再是ptrj为止
        while (i != str.length && (ptr[j] == str[i] || ptr[j] == '.')){
            //如果等,或者是.,都看看变0,1,2,3,4个能成吗,通过先调用process,再加i的办法来实现
            if (process(str, ptr, i, j + 2)) return true;//如果j*变0个,就可以,直接就可以了
            //否则得递增
            i++;//让i+1,2,3等等等,含义就是匹配str[i + k] ptr[j + 2],能行吗,作用是j*变k个j出来能匹配str吗
        }

        return process(str, ptr, i, j + 2);//j*必须变为0个i字符
    }

这个题理解暴力递归就够难的了,转DP今后复习再来转,先把暴力递归捋清楚,f的定义很重要。
后面我再来重新写下面dp代码怎么来的:

可以跳过下面的代码:【不过不管怎样,我还是硬着头皮讲一下这个代码怎么来的?】
在动态规划dp填表过程中,暴力递归怎么写?表就怎么填

我们看process函数:
2个样本,ij,里面j取到了M,i取到了N,主函数调f(0,0),故我们需要填表dp的为N+1 * M+1的表格
让str做行,i就是行下标
让pattern做列,j就是列下标
我们最后要dp[0][0] :代表str[0–N-1]能否由pattern[0–M-1]变化匹配而来???true or false

看下面的图,咱们要dp[0][0],肯定要从最后一样,最后一列开始填,然后找dp[i][j]依赖谁?
然倒回来填表。

(1)看N,M那个格子,代表pattern[M]=“”,空串,它能生成str[i–M-1]哪些字符串呢?
它一个空串,只能生成str中的一个空串“”,即i=N那个地方,代表N–N就一个空串,故dp[N][M]=true;
(2)看M列其他格子,空串不能生成有字符的字符串,所以趋势false;
(3)看N行,M-1那个格子,由于M-1只有pattern的一个字符,无法组成x星这种,所以不可能变为str的哪个字符串的,即false;
(4)看N-1行,M-1列那个格子,既然是对应位最后一个字符,如果pattern[M-1]=str[N-1]或者,pattern[M-1]=.,这样pattern[M-1]还是可以变str[N-1]的,否则就是false。
(5)看M-1列的其他格子,由于M-1列也就对应pattern一个字符,不可能出现x星,故i行M-1列都为false。一个字符不可能变出更多字符来。
(6)看N行其他的格子,str是空串“”,而pattern中,至少j<=M-1可能会有x星这种出现,因此只要是j+1位置为一个星,那无论前面j是啥,x星,或者.星,都能当做x星,让其变为空串“”,故填表时,看j+1位置的情况(pattern中的j+1位置的字符);
填表时,从右往左填表,当j处非星,且j+1处事*,则dp[N][j]=true;一旦有一个是false,前面的都不用去填了,默认就是false;
图3
——上面这些格子,我们初始化一下:

//填表的函数可以单独列出来,简介
    public static boolean[][] initDP(char[] str, char[] ptr){
        int N = str.length;
        int M = ptr.length;

        boolean[][] dp = new boolean[N + 1][M + 1];//因为终结位置是要用的
        //最后一列,只看最后一行交叉的位置
        dp[N][M] = true;//没的说,basecase,双双都是结尾,耗尽,空串是匹配的
        //最后一行--因为最后倒数第二列,M-1需要填写,所以我从右边开始判断,j*是否一直不断
        //j*j*出现,可以让exp直接变为0个字符,这样就是str耗尽的空串状况
        for (int j = M - 2; j >= 0; j -= 2) {
            //每次跳2跳核查
            if (ptr[j] != '*' && ptr[j + 1] == '*') dp[N][j] = true;//j*格式就没问题
            else break;//一旦出现不适j*的格式,之前的格子都不行了
        }

        //M-1列,先看最后一个格子dp[N][M-1]因为j是结尾字符,但是此刻str耗尽了已经
        //故,只会是F
        dp[N][M - 1] = false;
        //再看倒数第2行dp[N-1][M-1],俩都是最后一个字符,exp是.或者与stri相等就行,否则F
        dp[N - 1][M - 1] = ptr[M - 1] == '.' || ptr[M - 1] == str[N - 1];
        //其他的格子,代表啥呢,j是最后一个位置,但是i还有很多字符,不是结尾位置,显然不能变为这么多字符,且违反了basecase,全F

        return dp;
    }

(7)一圈都填好了,请问任意位置ij怎么填呢
看process,显然dp[i][j]会依赖i+1,j+1位置,这是当j+1是非星的时候的那段代码;【上面图中ij的右下角那个格子】
dp[i][j]还会依赖i+0,1,2,3等这些行,j+2列,【上面图中j+2那一列全部都依赖】
然后看process怎么填,咱就怎么填。

//改DP,既然是两个字符串,显然就是样本位置对应模型
    //str做行,exp做列,dpij表示:str从i到N能匹配ptr的j到M吗
    //N=str.len,M=ptr.len
    //这里要搞清楚这个表怎么填写,就要根据暴力递归的依赖,才知道ij如何填,这样才知道ij依赖哪些位置
    //basecase中,当j==M时,看i==N与否,就知道dp[i][M]是T/F了
    //注意递归中暴力递归调用了这俩函数:process(str, ptr, i + 1, j + 1);
    //(process(str, ptr, i, j + 2))。故 ij依赖谁呢,右下角,和while中的i,j+2这一列
    //你草稿纸画一个表,就知道这个表得填倒数第二列,最后一行,才行,否则咱们搞不定
    //最后主函数,需要return dp[0][0],所以是从下往上,从右往左填表的

    //dp改编
    public static boolean isMatchDP(String s, String p){
        if (s == null || p == null) return false;
        char[] str = s.toCharArray();
        char[] ptr = p.toCharArray();

        if (!isValid(str, ptr)) return false;//检查合法性

        boolean[][] dp = initDP(str, ptr);//先填别的格子
        int N = str.length;
        int M = ptr.length;

        //然后倒回来填
        for (int i = N - 1; i >= 0; i--) {
            for (int j = M - 2; j >= 0; j--) {
                //暴力递归怎么填,这里就怎么填,但是i!=N了,已经不要考虑上面的这个条件了
                //然后分两种情况,一个是j+1位置非*
                //既然j+1就不是*,那j位置必须要和i匹配,要么是相等,要么是.,否则配不上,当然保证i还有东西才行,且,后续位置也能配上才可以哦
                if (ptr[j + 1] != '*') dp[i][j] = (ptr[j] == '.' || ptr[j] == str[i]) && dp[i + 1][j + 1];//后续
                else {
                    //带j*
                    //然后是第二种情况,一个是j+1位置恰好就是*
                    //1)加入ptrj!=stri,这个时候,j*必须变为0个i字符才行
                    //2)当ptrj==stri的话,有很多种无限情况,j*可能变为0个字符,1个,2个,……无穷个,直到你前缀不再是ptrj为止
                    int si = i;
                    while (si != str.length && (ptr[j] == str[si] || ptr[j] == '.')){
                        //如果等,或者是.,都看看变0,1,2,3,4个能成吗,通过先调用process,再加i的办法来实现
                        if (dp[si][j + 2]) {
                            dp[i][j] = true;//如果j*变0个,就可以,直接就可以了
                            break;
                        }
                        //否则得递增
                        si++;//让i+1,2,3等等等,含义就是匹配str[i + k] ptr[j + 2],能行吗,作用是j*变k个j出来能匹配str吗
                    }
                    //上面都是false的话,看
                    if (dp[i][j] == false) dp[i][j] = dp[si][j + 2];//j*必须变为0个i字符的情况
                }
            }
        }

        return dp[0][0];
    }

本表格能填出来的前提,是一定理解透了我们写的出来的暴力递归代码的含义,
尤其理解f是从pattern的j–M-1范围上去变str的i–N-1范围。

测试代码:

public static void test(){
        String s = "aab";
        String p = "c*k*a*b*";
        System.out.println(isMatch(s, p));
        System.out.println(isMatchDP(s, p));
    }

    public static void main(String[] args) {
        test();
    }

总结

提示:重要经验:

1)暴力递归写样本位置对应模型,在尝试时发现,j位置可能要考虑j+1位置的情况,那就需要定义为j–M-1上的匹配;
2)而在尝试时发现,j位置可能要考虑j-1位置的情况,那就需要定义为0–j上的匹配;
这俩过程需要磨炼,大多数情况下是情况2,但是这个题目就是说的1),要仔细理解。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

冰露可乐

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值