Code Ganker: Regular Expression Matching -- LeetCode

2014年2月19日星期三

Regular Expression Matching -- LeetCode

原题链接: http://oj.leetcode.com/problems/regular-expression-matching/
这个题目比较常见,但是难度还是比较大的。我们先来看看brute force怎么解决。基本思路就是先看字符串s和p的从i和j开始的子串是否匹配,用递归的方法直到串的最后,最后回溯回来得到结果。假设现在走到s的i位置,p的j位置,情况分为下列两种:
(1)p[j+1]不是'*'。情况比较简单,只要判断当前s的i和p的j上的字符是否一样(如果有p在j上的字符是'.',也是相同),如果不同,返回false,否则,递归下一层i+1,j+1;
(2)p[j+1]是'*'。那么此时看从s[i]开始的子串,假设s[i],s[i+1],...s[i+k]都等于p[j]那么意味着这些都有可能是合适的匹配,那么递归对于剩下的(i,j+2),(i+1,j+2),...,(i+k,j+2)都要尝试(j+2是因为跳过当前和下一个'*'字符)。
实现代码如下:
public boolean isMatch(String s, String p) {
    return helper(s,p,0,0);
}
private boolean helper(String s, String p, int i, int j)
{
    if(j==p.length())
        return i==s.length();
    
    if(j==p.length()-1 || p.charAt(j+1)!='*')
    {
        if(i==s.length()|| s.charAt(i)!=p.charAt(j) && p.charAt(j)!='.')
            return false;
        else
            return helper(s,p,i+1,j+1);
    }
    //p.charAt(j+1)=='*'
    while(i<s.length() && (p.charAt(j)=='.' || s.charAt(i)==p.charAt(j)))
    {
        if(helper(s,p,i,j+2))
            return true;
        i++;
    }
    return helper(s,p,i,j+2);
}
接下来我们考虑如何优化brute force算法,熟悉动态规划的朋友可能发现了,其实这个思路已经很接近动态规划了。动态规划基本思想就是把我们计算过的历史信息记录下来,等到要用到的时候就直接使用,不用重新计算。在这个题里面,假设我们维护一个布尔数组res[i][j],代表s的前i个字符和p的前j个字符是否匹配(注意这里res的维度是s.length()+1,p.length()+1)。递推公式跟上面类似,分三种种情况:
(1)p[j+1]不是'*'。情况比较简单,只要判断如果当前s的i和p的j上的字符一样(如果有p在j上的字符是'.',也是相同),并且res[i][j]==true,则res[i+1][j+1]也为true,res[i+1][j+1]=false;
(2)p[j+1]是'*',但是p[j]!='.'。那么只要以下条件有一个满足即可对res[i+1][j+1]赋值为true:
    1)res[i+1][j]为真('*'只取前面字符一次);
    2)res[i+1][j-1]为真('*'前面字符一次都不取,也就是忽略这两个字符);
    3)res[i][j+1] && s[i]==s[i-1] && s[i-1]==p[j-1](这种情况是相当于i从0到s.length()扫过来,如果p[j+1]对应的字符是‘*’那就意味着接下来的串就可以依次匹配下来,如果下面的字符一直重复,并且就是‘*’前面的那个字符)。
(3)p[j+1]是'*',并且p[j]=='.'。因为".*"可以匹配任意字符串,所以在前面的res[i+1][j-1]或者res[i+1][j]中只要有i+1是true,那么剩下的res[i+1][j+1],res[i+2][j+1],...,res[s.length()][j+1]就都是true了。
这道题有个很重要的点,就是实现的时候外层循环应该是p,然后待匹配串s内层循环扫过来。代码如下:
public boolean isMatch(String s, String p) {
    if(s.length()==0 && p.length()==0)
        return true;
    if(p.length()==0)
        return false;
    boolean[][] res = new boolean[s.length()+1][p.length()+1];
    res[0][0] = true;
    for(int j=0;j<p.length();j++)
    {
        if(p.charAt(j)=='*')
        {
            if(j>0 && res[0][j-1]) res[0][j+1]=true;
            if(j<1) continue;
            if(p.charAt(j-1)!='.')
            {
                for(int i=0;i<s.length();i++)
                {
                    if(res[i+1][j] || j>0&&res[i+1][j-1] 
                    || i>0 && j>0 && res[i][j+1]&&s.charAt(i)==s.charAt(i-1)&&s.charAt(i-1)==p.charAt(j-1))
                        res[i+1][j+1] = true;
                }
            }
            else
            {
                int i=0;
                while(j>0 && i<s.length() && !res[i+1][j-1] && !res[i+1][j])
                    i++;
                for(;i<s.length();i++)
                {
                    res[i+1][j+1] = true;
                }
            }
        }
        else
        {
            for(int i=0;i<s.length();i++)
            {
                if(s.charAt(i)==p.charAt(j) || p.charAt(j)=='.')
                    res[i+1][j+1] = res[i][j];
            }
        }
    }
    return res[s.length()][p.length()];
}
对比以上两种做法,其实思路基本类似,动态规划优势在于对于前面计算过得信息不需要再重复计算,而brute force则每次重新计算。上面两种算法中,动态规划的时间复杂度是O(m*n),空间复杂度也是O(m*n),假设s的长度为n, p的长度为m。而brute force的递归算法最坏情况是指数量级的复杂度。
这种题目在面试中算是比较难的题目,因为分情况比较多,如果不熟悉比较难在面试中理清思路并且做对,我不是很喜欢,但是在面经中却经常看到,所以还是得搞熟悉比较好。类似的题目有Wildcard Matching,那个还更简单一些,不过思路是基本一致的,只是少分一点情况。

22 条评论:

  1. 楼主能解释一下 为什么res的维度需要是s.length()+1,p.length()+1)。这样看代码时很费劲啊,因为和字符串的indx错一位。
    还有代码解释部分的index和代码本身不一致,例如,这句:
    (1)p[j+1]不是'*'。情况比较简单,只要判断如果当前s的i和p的j上的字符一样(如果有p在j上的字符是'.',也是相同)

    在代码中用的是: p[j]不是'*'

    回复删除
    回复
    1. 你好哈,你说的这个是动态规划实现中常用的一个技巧,就是给定一个起始的条件(比如代码中的res[0][0]=true), 然后按照这个条件递归能够满足递推式,那么就省去我们需要对第一行做一次手动赋值的过程,代码会比较简练,但是理解上会难一点,因为需要对起始条件有正确的理解。其实这里也可以手动对第一行进行赋值,然后维度就只需要[s.length()][p.length()]。用s.length()+1,p.length()+1)的好处是给了res[0][0]=true之后就可以按照递归式进行规划了。
      对于index的问题,我是不想在思路上带入一些实现的细节,一般我们分析算法时都是下标从1开始,代码中因为从0开始,所以p的第j+1个元素实际上就是p[j]了。比较好的练习方法应该是理解了递推式之后自己进行实现,因为实现的方法其实是基于对算法的理解的。有什么问题欢迎继续交流哈。

      删除
    2. 明白了。感觉楼主的coding水平很高啊,对各种情况都考虑到了,这也是我在网上发现的唯一一个用动态规划实现的。Max Points on a Line那道题的实现也很有特色。

      删除
    3. 过奖了哈~ 尘世中一只迷途的小码农~

      删除
  2. 请问博主 brute force 最坏时间复杂度是 指数级的结论是怎么得出来的呢?我的推断是 T(n) = T(n-1) + ... + T(1),用数组中的解法,若 S(n) = sum(T(i)) (1<=i<=n-1),得出 T(n) = O(2^n), 不晓得这种推断是否合理,还请博主指教

    回复删除
    回复
    1. 恩, 基本上是你的这个思路哈, 不过这个也没法定量, 因为递归回溯的次数取决于字符串的情况, 最坏情况肯定是指数的, 跟你的推理差不多,当然也有可能好的不用回溯的哈。

      删除
  3. Hi 楼主 能否请教一下其中的一个例子isMatch("ab", ".*") → true. 关于这个我不太明白
    假如'.' 匹配了'a'. 那么'.*'就是关于 a的字符串啊。 为什么还能匹配到'b' 呢?

    回复删除
    回复
    1. 他的这里不需要是同一个字符的重复,因为.可以代表任何字符,所以.*可以代表任何字符串哈~

      删除
    2. 啊,.* 代表万能字符。 请教下楼主另一个问题
      (3)p[j+1]是'*',并且p[j]=='.'。因为".*"可以匹配任意字符串,所以在前面的res[i+1][j-1]或者res[i+1][j]中只要有i+1是true,那么剩下的res[i+1][j+1],res[i+2][j+1],...,res[s.length()][j+1]就都是true了。
      这句话我明白 可是代码为什么是
      int i=0;
      while(j>0 && i<s.length() && !res[i+1][j-1] && !res[i+1][j])
      i++;
      for(;i<s.length();i++)
      {
      res[i+1][j+1] = true;
      }
      为什么是 !res[i+1][j-1] && !res[i+1][j]。 而且这个while 也只是执行了一次而已啊。感觉代码和描述的做法不一样啊,看了半天没看懂

      删除
    3. 诶,我好像明白了。 是不是 abc 和 de.* 也是匹配的关系?

      删除
    4. 没有, abc和de.*应该是不匹配的~

      删除
    5. 额。。。那我就不明白while(j>0 && i<s.length() && !res[i+1][j-1] && !res[i+1][j])
      i++; 是什么意思了

      删除
    6. while(j>0 && i<s.length() && !res[i+1][j-1] && !res[i+1][j]) i++;
      这句话就是实现“前面的res[i+1][j-1]或者res[i+1][j]中只要有i+1是true”这句话的,注意这里是一个while循环然后i++,他会走到第一个true的地方~ 接下来,再把剩下的全部置成true哈~

      删除
    7. 可是那不就是说 s的前i +1 个 和 p的前 j + 1 个是不匹配的么 ,反而 res[i+2][j+1] res[i+3][j+1]... 却为true。 这不矛盾了么

      删除
    8. 是那些不匹配的都跳过去,当找到一个是true的时候,就意味着除了.*前面是可以匹配上的,那么剩下的就都可以了,因为无论剩下是什么,用.*就可以搞定哈~

      删除
  4. 请问楼主brute force的版本里最后为什么要return helper(s,p,i,j+2)?

    回复删除
    回复
    1. 因为上面循环停下来之后还要把最后一次做一下~ 测试一下简单的case就知道了哈~

      删除
  5. Hi, I use res[i][j+1] && s[i]==p[j-1] to replace res[i][j+1] && s[i]==s[i-1] && s[i-1]==p[j-1], the code also works. Do you think it is correct?

    回复删除
    回复
    1. 恩,应该是对的~ 只有前面重复才能使得res[i][j+1]是true,所以s[i]==s[i-1]好像是隐含的条件哈~

      删除
  6. 你好我想问一下初始化为什么是这么设置的
    if(j>0 && res[0][j-1]) res[0][j+1]=true;
    第一行为什么要隔一个有一个1

    回复删除
    回复
    1. 这里并不是各一个就有一个1哇,而是当当前符号是*号时,后面其实可以匹配了~ 只需要看res[0][j-1],如果是true,那么res[0][j+1]就是true了~

      删除