力扣-10 正则表达式匹配

题目:给定一个字符串 (s) 和一个字符模式 (p)。实现支持 '.' 和 '*' 的正则表达式匹配。

'.' 匹配任意单个字符。

'*' 匹配零个或多个前面的元素。

匹配应该覆盖整个字符串 (s) ,而不是部分字符串。

说明:

  • s 可能为空,且只包含从 a-z 的小写字母。
  • p 可能为空,且只包含从 a-z 的小写字母,以及字符 . 和 *

 

此博客有提供了两种解法:深搜 和 动态规划。

动态规划解法比较优秀,建议直接看动态规划的解法。

一、深度优先搜索

从题目可知,需要注意的有三个情况:“a”,“.,“*”(a代表普通字符)

 

现在假设有 s = abc p = abc

可见,s[0] = a = p[0]。

因为第一个字符匹配,所以可以将问题转化称 s = bc p = bc 的子问题。

所以我们只要解决第一个字符的匹配情况,然后递归匹配其子问题即可。

 

s与p匹配的时候,s只检查字串的第一个字符即可,而p串则需要检查第1个和第2个字符。

在我的思路中:

p第一个字符可能的情况:“a”、“.”。    下面会说到“*”为什么不可能。

P第二个字符可能的情况:“a”、“.”、“*”。

 

如果第二个字符是“*”,则说明第一个字符的数量处于 0 - ∞ 的特殊状态。所以需要特殊处理。

即,有两种情况:一、第二个字符不为“*”和第二个字符为“*”。

 

现在看第一种情况:p第二个字符不为“*”。

这种情况只需要处理第一个字符即可,第二个字符对第一个字符无任何影响。

此时,如果s[0] == p[0] 或 p[0] = ‘.’,则s与p第一个字符匹配。直接将匹配的字符去掉,继续递归匹配子串。

        否则,s与p第一个字符不匹配,说明此深搜路线不通。

 

现在看第二种情况:p第二个字符为“*”

这种情况分三种子情况:

  • a* 生效了0次
  • a* 生效了1次
  • a* 生效了n次

所以,在此处需要引出三个分支:

  1. a* 生效了0次,说明a* 没有什么用,不论a是什么都不会匹配掉s的第一个字符。所以p串的第一字符和第二字符(*)会被舍掉,s串不动。
  2. a* 生效了1次,说明,此时s[0] = p[0] ,在a* 匹配掉s串的第一个字符串后a*不再起作用,所以s串舍掉第一个字符,p串舍掉第一个和第二个(*)字符。
  3. a* 生效了n次,说明,此时s[0] = p[0],在a* 匹配掉s串的第一个字符后,会继续尝试生效。所以s串舍掉第一个字符,p串不动。

从上面可知,在a* 的情况下,只要p发生舍弃,肯定就会舍弃a* 这两个字符,所以“*”必定会被附带着舍弃掉,所以只要输入的是合法的表达式,就绝对不会出现第一个字符为 * 的情况。

 

因为每次递归只处理s第一个字符和p前两个字符,所以简单来说,只要理清下面几种情况的处理方法后,就可以写出这个方法:(xxxx表示任意字符串组合)

S字符串

P字符串

思路

axxxx

abxxxx

直接匹配掉第一个字符,处理子串

axxxx

.bxxxx

同上

axxxx

bbxxxx

无法匹配,此深搜路线不通,舍弃路线

aaxxx

a*xxxx

a* 可以匹配掉:空、a、a…。

此时应处理为:匹配空(0)、匹配一次a后a*被匹配掉(1)、匹配一次a后 a* 不被匹配掉(n)。

axxxx

.*xxxx

.* 同上

axxxx

b*xxxx

b* 只能匹配掉:空

“”

abxxxx

无法匹配,此深搜路线不通

“”

a*xxxx

a* 会匹配掉空,

axxxx

“”

无法匹配,此深搜路线不通

“”

“”

匹配成功,字符串符合正则表达式(唯一出口)

可能有遗漏,大家根据情况补全即可。

 

下面谈谈,这种做法的效率问题:

首先,我们看下面的情况:

    S = abxxxx 和 P = a*xxxx

    可见,此时P种的 a* 可以匹配为:0 1 n个a。

    当匹配为1的时候:S = bxxxx 和 P = xxxx

    当匹配为n的时候:S = bxxxx 和 P = a*xxxx。此时,可以在下一次匹配时进一步匹配成:S = bxxxx 和 P = xxxx。

我们看到,S = bxxxx 和 P = xxxx 的情况出现了两次。而且这种重复的情况在执行过程种会大量的出现。大量的重复处理必然导致效率降低。而且是指数形式降低。

1 分为 2 ,2 分为 4 … 重复几次,便成了极为庞大的浪费。

 

 

代码:

 

   


private boolean subMatch(int sIndex,int pIndex , String s, String p){
    int pNextIndex = pIndex + 1;
    if(sIndex == s.length() ^ pIndex == p.length()){
        if(pNextIndex < p.length() && p.charAt(pNextIndex) == '*'){
            return subMatch(sIndex, pIndex + 2, s, p);
        }
        return false;
    }
    if(sIndex == s.length() && pIndex == p.length())
        return true;
    char sChar = s.charAt(sIndex);
    char pChar = p.charAt(pIndex);
    if(pNextIndex != p.length()){
        if(p.charAt(pNextIndex) != '*'){
            //下一个字符不是'*';
            if(sChar == pChar || pChar == '.'){
                //当前字符相等或模式字符为'.',匹配;
                if(subMatch(sIndex + 1, pIndex + 1, s, p))
                    return true;
            }else{
                //不匹配
                return false;
            }
        } else {
            //下一个字符是'*';
            if(sChar == pChar || pChar == '.'){
                //当前字符相等或模式字符为'.',匹配;
                if(subMatch(sIndex + 1, pIndex, s, p))
                    return true;
                if(subMatch(sIndex, pIndex + 2, s, p))
                    return true;
                if(subMatch(sIndex + 1, pIndex + 2, s, p))
                    return true;
            } else {
                //不匹配
                if(subMatch(sIndex, pIndex + 2, s, p))
                    return true;
            }
        }
    } else {
        if(sChar == pChar || pChar == '.'){
            if(subMatch(sIndex + 1, pIndex + 1, s, p)){
                return true;
            } else {
                return false;
            }
        } else {
            return false;
        }
    }
    //this place
    return false;
}

public boolean isMatch(String s, String p) {
    return subMatch(0,0, s, p);
}

二、动态规划

    因为上面思路的大量重复子问题的出现,且每一步的匹配是否成功,均要取决于其子结构是否成功。所以符合动态规划的核心思想:重复子问题 和 最优子结构。

   

所以只要能列出动态规划的方程,即可很快的码出代码,解决问题。

下面动态方程取自题解中的 powcai 大神:

 

  1. p[j] == s[i]:dp[i][j] = dp[i-1][j-1]
  2. p[j] == ".":dp[i][j] = dp[i-1][j-1]
  3. p[j] =="*":

3.1 p[j-1] != s[i]:dp[i][j] = dp[i][j-2]

3.2 p[i-1] == s[i] or p[i-1] == ".":

dp[i][j] = dp[i-1][j] // 多个a的情况

or dp[i][j] = dp[i][j-1] // 单个a的情况

or dp[i][j] = dp[i][j-2] // 没有a的情况

 

   

dp[i][j]表达的意思是: s的i之前的字串和p的j之前的字串是否匹配。

可以看出,整体分为了三种情况,前两种可以看作一种情况:

如果p[j] == s[i] || p[j] == “.”,则说明匹配,取dp[i][j]=dp[i-1][j-1]。

如果p[j] == “*”,则说明需要用到p[j-1]的字符:

    如果p[j-1] != s[i],说明 a* 无法匹配任何字符,取

    dp[i][j] = dp[i][j-2](两个字符合起来无法匹配任何字符,所以-2)

    如果p[j-1]==s[i] || p[j-1]==“.”。说明a*可以匹配字符,那么dp[i][j]的取值可能来自三个地方:

  • 0个匹配:因为没有匹配,所以dp[i][j]=dp[i][j-2](因为没有匹配,i不动)
  • 1个匹配:一个匹配说明dp[i][j]是来自dp[i-1][j-1]的。
  • N个匹配:说明dp[i][j] = dp[i-1][j]。

        三个地方,只要有一个为 真 说明就可以匹配。 

代码:

public boolean isMatch(String s, String p){
        s = "#" + s;
        p = "#" + p;
        boolean[][] dp = new boolean[s.length() + 1][p.length() + 1];
        dp[0][0] = true;

        for(int i = 2; i < p.length(); i += 2){
            if(p.charAt(i) == '*' && dp[0][i - 2])
                dp[0][i] = true;
            else
                break;
        }

        for(int i = 1; i < s.length(); i++){
            for(int j = 1; j < p.length(); j++){
                if(p.charAt(j) != '*'){
                    if(s.charAt(i) == p.charAt(j) || p.charAt(j) == '.'){
                        dp[i][j] = dp[i - 1][j - 1];
                    }
                } else {
                    if(s.charAt(i) == p.charAt(j - 1) || p.charAt(j - 1) == '.'){
                        dp[i][j] = dp[i][j - 2] | dp[i][j - 1] | dp[i - 1][j];
                    } else {
                        dp[i][j] = dp[i][j - 2];
                    }
                }
            }
        }

 

你可能感兴趣的:(LeetCode)