重新编辑regular expression match

本文深入探讨了LeetCode上的正则表达式匹配问题,详细介绍了两种解决方案:暴力递归和动态规划。通过实例解析了各种匹配场景,并给出了易于理解的代码实现。

问题链接:https://oj.leetcode.com/problems/regular-expression-matching/

问题描述:Implement regular expression matching with support for '.' and '*'.

'.' Matches any single character.
'*' Matches zero or more of the preceding element.

The matching should cover the entire input string (not partial).

The function prototype should be:
bool isMatch(const char *s, const char *p)

Some examples:
isMatch("aa","a") → false
isMatch("aa","aa") → true
isMatch("aaa","aa") → false
isMatch("aa", "a*") → true
isMatch("aa", ".*") → true
isMatch("ab", ".*") → true
isMatch("aab", "c*a*b") → true

API : public boolean isMatch(String s, String p)

 

这题其实有点绕和难。leetcode归类为hard应该是没有错的,除非我太水了。。。

先介绍第一种做法:Brute Force.

毕竟怎么看都是要记录当前扫的s和p的位置的,那么helper函数的长相应该是这样的:public boolean helper(String s, String p, int i, int j)

i表示扫到s哪个位置,j表示扫到了p哪个位置。

而根据题意,根据条件case有很多,譬如s[i] == p[j], p[j] == '.', p[j] == '*' 等等

但大致上可以基本上可以分类为两个case:

1. s[i] == p[j] 或者p[j] == ','。 在这种情况下,以i + 1和j + 1的前提往下递归一层即可。

2. p[j + 1] == '*',之所以这里用j + 1做判断是因为'*'的内容是根据p[j]来的。这个时候'*'表示的是0~无限多个p[j]。所以s[i], s[i + 1]....s[s.length() - 1]都有可以和p[j + 1]匹配的可能。只要满足以下两个条件的其中之一即可p[j] == '.' 或者 p[j] ==s[i + k] (0 <= k <= s.length() - 1 - i)。 所以这个时候需要不停循环并递归s[i + 1]下一层直到不满足上述两个条件之一。

3. 上述两个case都不满足的就很简单了, return false即可。

4. 这个递归的终结点base case是if(j == p.length()) return i == s.length()。 之所以用p作为先决条件很大程度上是因为'*'的存在可以表示任意个字符(包括0个),随意性很强。这样比较容易判断。所以用if(j == p.length() && i == s.length()) 或者if(i == s.length) return j == p.length()都是不恰当的。

给出代码如下:

 

   public boolean isMatch(String s, String p) {
        return helper(s, p, 0, 0);
    }
    
    public boolean helper(String s, String p, int l, int r){
	//2017-10-17
	//还是补充一下这里,重新回顾的时候发现先判断r == p.length()还是有意义的
	//我想了一会儿为什么当年写的时候不用 if(l == s.length() && r == p.length()) return true 去判断
	//后来发现这个条件是必要不充分的,也就是即使l == s.length() && r != p.length()的时候,也是可以返回true的
	//就是在l == s.length()的时候,如果r在p的位置的后方全是带有*的组合。
	// ex. s = "abcd", p = "abcc*d*d*"之类的,helper(s, p, 4, 4)也是会返回true的
	// 所以用上面的if去判断,会错误的返回false。
	if(r == p.length()) 
	   return l == s.length();
	if(r + 1 < p.length() && p.charAt(r + 1) == '*') {
	    while(l < s.length() && (p.charAt(r) == '.' || p.charAt(r) == s.charAt(l))) {
	      if(helper(s, p, l, r + 2))
		  return true;
	      l++;
	    }
	    return helper(s, p, l, r + 2);
	} else if(l < s.length() && (p.charAt(r) == '.' || p.charAt(r) == s.charAt(l))) {
	    return helper(s, p, l + 1, r + 1);
	} else
	    return false;
   }


上述算法的复杂度是指数级别的,但是也能过leetcode测试,说明本题leetcode的检测并没有很严格。
接下来将要介绍dp的算法,其实所有的dp算法大都可以从brute force算法中看出端倪所在。dp推导式的条件随后放送。
来撸dp推导式了,dp最重要的就是推导式,有了推导式代码其实也就成型了。
然而dp的推导式基本都是可以从brute force得到的。
首先定义dp函数f(i,j)的定义,f(i,j)在这里表示的就是s[1...i]到p[1...j]是否一个可行的regular expression match
那么根据之前在brute force那样,我们可以继续分出类似的case来得到推导式:
Case 1 : 就是当p[j + 1] != '*'的情况,
这个情况比较简单,推导式就是:f(i + 1, j + 1) = f(i, j) && (s[i] == s[j] || p[j] == ".")
Case 2 : 当p[j + 1] == "*"的情况
这个情况就和之前brute force有点不一样了,还需要分两个sub case来分析。
Case 2.a : p[i] == '.'
这种情况比较好解决,其实就和brute force可以循环i到底一样,f(i + k, j + 1) = f(i + 1, j) || f(i + 1, j - 1)   (i + 1 <= i + k <= s.length())
Case 2.b : p[i] != '.'
这种情况下,以下三个条件任意一个成立都可以:
1.f(i + 1, j) 成立, 这个情况表示星号之前的那个可以match到s[i],也就是星号至少可以取一个之前的字符。
2.f(i + 1, j - 1)成立, 这种情况表示星号的前面第二个可以match到s[i], 也就是星号和之前的字符就当成不存在处理,也就是子串"x*"(x在这里表示任意一个字符)部分当成空字符串处理。
3.f(i, j + 1) && s[i] == s[i] && s[i - 1] == p[j - 1]成立,这种情况比较难理解。其实就是s里前一个字符和星号之前的字符相符,然后相同的字符在s[i - 1, i .... i + k]里循环下去。

 

根据以上的case分析。给出代码如下:

 

 

    public boolean isMatch(String s, String p) {
        if(s.length() == 0 && p.length() == 0)
            return true;
        if(p.length() == 0)
            return false;
        boolean[][] dp = new boolean[s.length() + 1][p.length() + 1];
        dp[0][0] = true;
        for(int j = 0; j < p.length(); j++){
            if(p.charAt(j) == '*'){//case 2
                dp[0][j + 1] |= j > 0 && dp[0][j - 1];
                if(j == 0)continue;
                if(p.charAt(j - 1) != '.'){//case 2b.
                    for(int i = 0; i < s.length(); i++){
                        dp[i + 1][j + 1] |= dp[i + 1][j] || dp[i + 1][j - 1] || (i > 0 && dp[i][j + 1] && s.charAt(i) == s.charAt(i - 1) && s.charAt(i - 1) == p.charAt(j - 1));
                    }
                }else{//case 2.a
                    int i = 0;
                    while(j > 0 && i < s.length() &&!dp[i + 1][j - 1] && !dp[i + 1][j])// 找到第一个匹配条件的,也就是找到第一个为真的
                        i++;
                    for(; i < s.length(); i++){
                        dp[i + 1][j + 1] = true;
                    }
                }
            }else{// case 1
                for(int i = 0; i < s.length(); i++){
                    dp[i + 1][j + 1] = dp[i][j] && (s.charAt(i) == p.charAt(j) || p.charAt(j) == '.');
                }
            }
        }
        return dp[s.length()][p.length()];
    }


下面一段code是我另外再写的,附加了很多comments的。

 

 

 

 

    public boolean isMatch(String s, String p) {
        boolean[][] subResult = new boolean[s.length() + 1][p.length() + 1];
        subResult[0][0] = true;//空集对空集当然是true了
        for(int i = 1; i < p.length(); i += 2){
            if(p.charAt(i) != '*'){
                break;
            }
            subResult[0][i + 1] = true;
        }
        for(int i = 1; i <= p.length(); i++){
            if(p.charAt(i - 1) == '*'){
//                if(i > 1 && subResult[0][i - 2])subResult[0][i] = true;
//                if(i == 1)continue;
                //这个算是一个base case,还是很有考究的。
                //基本上是可以这么理解的。
                //subResult第一维如果是0,就表示s取子集的时候取了空集
                //而这个时候,事实上就是当p[1], p[3], p[5]...p[2 * i - 1]
                //subResult[0][2 * i]都为true
                //而当其中一环断节的时候,接下来的都不为true了。
                //另外,事实上有效的输入里,i都是除以2余1的,如果全部为真的话
                if(p.charAt(i - 2) != '.'){
                    for(int j = 1; j <= s.length(); j++){
                        if(subResult[j][i - 1] || subResult[j][i - 2]){
                            subResult[j][i] = true;
                            //subResult[j + 1][i] 表示前面只取一个的时候为true
                            //也就是星号前面的字符是可以匹配当前s.charAt(j)的
                            //subResult[j + 1][i - 1]表示星号前面的字符一个都不取
                            //这种情况下就有好几种可能了。
                            //包括星号前面的前面的字符匹配
                            //星号前面的前面的字符还是星号,然后还能对当前
                            //s.charAt(j)匹配等等情况。
                        }
                        if(j > 1 && s.charAt(j - 1) == s.charAt(j - 2) && s.charAt(j - 2) == p.charAt(i - 2) && subResult[j - 1][i]){
                            subResult[j][i] = true;
                            //这个case是一个特殊的case。
                            //这个表示源字符串的某一个字符在不停重复
                            //然后这个字符和星号之前的字符是相同的
                        }
                    }
                }else{
                    //这个case就简单的多了
                    //星号前面是点号,就表示这两个字符组合可以匹配任何字符串
                    //所以就要找到第一个匹配到p字符串当前长度 - 1为真的情况
                    //因为点星组合可以匹配接下来的所有字符,所以剩下的都为真了
                    //在p到当前位置的子串的情况下
                    int j = 1;
                    while(i > 1 && j <= s.length() && !subResult[j][i - 1] && !subResult[j][i - 2])
                        j++;
                    for(; j <= s.length(); j++){
                        subResult[j][i] = true;
                    }
                }
            }else{
                //这种情况就比较好理解了,
                //只有三种情况,p.charAt(i - 1) == '.' 和 p.charAt(i - 1) == s.charAt(j - 1)是一样的,只要p.charAt(i - 2)和s.charAt(j - 2)对的上号,这里也是真
                //否则为假
                for(int j = 1; j <= s.length(); j++){
                    subResult[j][i] = subResult[j - 1][i - 1] && (p.charAt(i - 1) == s.charAt(j - 1) || p.charAt(i - 1) == '.');
                }
            }
        }
        return subResult[s.length()][p.length()];
    }

 

2017-10-25

new solution found in http://bangbingsyb.blogspot.com/2014/11/leetcode-regular-expression-matching.html

This is simpler and easier than mine

 

class Solution {
    public boolean isMatch(String s, String p) {
        boolean[][] dpResult = new boolean[s.length() + 1][p.length() + 1];
        dpResult[0][0] = true; // Empty vs Empty, return true
        for(int i = 0; i <= s.length(); i++) {
            for (int j = 1; j <= p.length(); j++) {
                if (p.charAt(j - 1) != '*') {
                    if (i > 0) {
                        boolean currentMatch = p.charAt(j - 1) == s.charAt(i - 1) || p.charAt(j - 1) == '.';
                        dpResult[i][j] = dpResult[i - 1][j - 1] && currentMatch;
                    }
                } else if (j > 1){
                    if (dpResult[i][j - 1] || dpResult[i][j - 2]) {
                        dpResult[i][j] = true;
                    } else if(i > 1 && (p.charAt(j - 2) == s.charAt(i - 1) || p.charAt(j - 2) == '.')) {
                        dpResult[i][j] = dpResult[i - 1][j];
                    }
                }
            }
        }
        
        return dpResult[s.length()][p.length()];
    }

}

 

The following is commented version:

 

 

class Solution {
    public boolean isMatch(String s, String p) {
        boolean[][] dpResult = new boolean[s.length() + 1][p.length() + 1];
        dpResult[0][0] = true; // Empty vs Empty, return true
        for(int i = 0; i <= s.length(); i++) {
            for (int j = 1; j <= p.length(); j++) {
                if (p.charAt(j - 1) != '*') {
                    // Case 1 : 非星号,只需要考虑s[i]和p[j]是否match以及s[0..i - 1]和p[0 .. j - 1]是否match
                    //s[i] == p[j] or p[j] == '.'
                    if (i > 0) {
                        boolean currentMatch = p.charAt(j - 1) == s.charAt(i - 1) || p.charAt(j - 1) == '.';
                        //dpResult[i - 1][j - 1]是s[0 .. i - 1]和p[0 .. j - 1] 的子结果
                        dpResult[i][j] = dpResult[i - 1][j - 1] && currentMatch;
                    }
                } else if (j > 1){
                    if (dpResult[i][j - 1] || dpResult[i][j - 2]) {
                    //这是带星号的检查。
                    //这是匹配了一个或者零个的情况。
                    //也就是如果s[0 .. i]能和p[0 .. j - 1]匹配上或者s[0 .. i]能和p[0 .. j - 2]匹配上,那么s[0 .. i]和p[0 .. j]就能匹配上
                    //举例子s = "a", p = "ab*",当i = 0, j = 0的时候dp[i][j] = true
                    //当i = 0, j = 2的时候,j - 2 = 0,此时是true, 所以实际上取了dp[i][j - 2]表示当前的星号匹配了0个。`b*`这个子串取空字符串
                    //另一个例子,s = "ab", p = "ab*"的时候,当 i = 1, j = 1的时候dp[i][j]依然是true
                    //此时,当i = 1, j = 2,的时候,因为dp[i][j - 1]("ab"对"ab") 是true,所以dp[i][j]("ab"对"ab*")依然还可以是true
                    //此时b*只取一个。
                        dpResult[i][j] = true;
                    } else if(i > 1 && (p.charAt(j - 2) == s.charAt(i - 1) || p.charAt(j - 2) == '.')) {
                    //这个表示如果之前子字符串s[0 .. i - 1]和p[0 .. j]匹配,当前字母也匹配,那么星号可以继续取多前面一个
                    //举个例子说明
                    //s = "abbbbb" p = "ab*"
                    //当i = 2 j = 2的时候,因为上面的条件判断我们可以知道dp[i - 1][j] = true("ab" 对 "ab*")
                    //而因为s[2] = b,是 p[i - 1] = 'b',可以继续往下match,所以我们可知dp[2][2] = true, 也就是"abb"对"ab*"
                    //需要说明的是,这里的dp[i][j]对应的是代码的dp[i + 1][j + 1],因为数组的index是0起步的
                    //但这里的dpResult[0][0]对应的shi
                        dpResult[i][j] = dpResult[i - 1][j];
                    }
                }
            }
        }
        
        return dpResult[s.length()][p.length()];
    }

}

2018-02-27 Updated

我才发现我最后还写了一个比上面都要来得好和简洁的答案,贴在下面以供参考,

    public boolean isMatch(String s, String p) {
        boolean[][] dp = new boolean[s.length() + 1][p.length() + 1];
        dp[0][0] = true;
        
        for (int i = 1; i <= p.length(); i++) {
            //j可以为0但i为0没有意义的原因在于
            //p可以用非空子字符串去匹配s空子字符串,只要一直是梅花间竹的星号。反之则不行
            //譬如s取"", p取"a*b*c*d*e*a*"之类的。这个依旧可以为真
            for (int j = 0; j <= s.length(); j++) {
                if (j > 0 && (s.charAt(j - 1) == p.charAt(i - 1) || p.charAt(i - 1) == '.')) {
                    dp[j][i] |= dp[j - 1][i - 1];
                } else if(p.charAt(i - 1) == '*') {
                    // dp[j][i - 1] 指的是s只拿之前一个匹配。 
                    // 举例说明: s = "abc" p = "abc*" dp[j][i - 1]表示s = "abc"和p拿"abc"出来比较为true的时候"abc"和"abc*"也为真
                    // dp[j][i - 2] 指的是s一个都不拿来匹配的情况。
                    // 举例说明: s = "ab" p = "abc*" dp[j][i - 2]表示s="ab"和p拿"ab"出来比较为true的时候"ab"和"abc*"也为真
                    if (dp[j][i - 1] || dp[j][i - 2]) {
                        dp[j][i] = true;
                    } else if (j > 1 && (s.charAt(j - 1) == p.charAt(i - 2) || p.charAt(i - 2) == '.')) {
                        //这是接下来的情况。 不断往下匹配。
                        dp[j][i] |= dp[j - 1][i];
                    }
                }
            }
        }        
        return dp[s.length()][p.length()];
    }

2018-09-17 updated:

思考过后发现最后一段代码的星号判断里,dp[j][i - 1]是可以不要的,因为那已经囊括在下面那个else if里了,所以只需要dp[j][i - 2]做判断即可。也就是

                    if (dp[j][i - 2]) {
                        dp[j][i] = true;
                    }

 

 

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值