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

这个题目比较常见,但是难度还是比较大的。我们先来看看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是因为跳过当前和下一个'*'字符)。 
实现代码如下:
  1. public boolean isMatch(String s, String p) {  
  2.     return helper(s,p,0,0);  
  3. }  
  4. private boolean helper(String s, String p, int i, int j)  
  5. {  
  6.     if(j==p.length())  
  7.         return i==s.length();  
  8.       
  9.     if(j==p.length()-1 || p.charAt(j+1)!='*')  
  10.     {  
  11.         if(i==s.length()|| s.charAt(i)!=p.charAt(j) && p.charAt(j)!='.')  
  12.             return false;  
  13.         else  
  14.             return helper(s,p,i+1,j+1);  
  15.     }  
  16.     //p.charAt(j+1)=='*'  
  17.     while(i<s.length() && (p.charAt(j)=='.' || s.charAt(i)==p.charAt(j)))  
  18.     {  
  19.         if(helper(s,p,i,j+2))  
  20.             return true;  
  21.         i++;  
  22.     }  
  23.     return helper(s,p,i,j+2);  
  24. }  
接
下来我们考虑如何优化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内层循环扫过来。代码如下: 
  1. public boolean isMatch(String s, String p) {  
  2.     if(s.length()==0 && p.length()==0)  
  3.         return true;  
  4.     if(p.length()==0)  
  5.         return false;  
  6.     boolean[][] res = new boolean[s.length()+1][p.length()+1];  
  7.     res[0][0] = true;  
  8.     for(int j=0;j<p.length();j++)  
  9.     {  
  10.         if(p.charAt(j)=='*')  
  11.         {  
  12.             if(j>0 && res[0][j-1]) res[0][j+1]=true;  
  13.             if(j<1) continue;  
  14.             if(p.charAt(j-1)!='.')  
  15.             {  
  16.                 for(int i=0;i<s.length();i++)  
  17.                 {  
  18.                     if(res[i+1][j] || j>0&&res[i+1][j-1]   
  19.                     || i>0 && j>0 && res[i][j+1]&&s.charAt(i)==s.charAt(i-1)&&s.charAt(i-1)==p.charAt(j-1))  
  20.                         res[i+1][j+1] = true;  
  21.                 }  
  22.             }  
  23.             else  
  24.             {  
  25.                 int i=0;  
  26.                 while(j>0 && i<s.length() && !res[i+1][j-1] && !res[i+1][j])  
  27.                     i++;  
  28.                 for(;i<s.length();i++)  
  29.                 {  
  30.                     res[i+1][j+1] = true;  
  31.                 }  
  32.             }  
  33.         }  
  34.         else  
  35.         {  
  36.             for(int i=0;i<s.length();i++)  
  37.             {  
  38.                 if(s.charAt(i)==p.charAt(j) || p.charAt(j)=='.')  
  39.                     res[i+1][j+1] = res[i][j];  
  40.             }  
  41.         }  
  42.     }  
  43.     return res[s.length()][p.length()];  
  44. }  
对比以上两种做法,其实思路基本类似,动态规划优势在于对于前面计算过得信息不需要再重复计算,而brute
 force则每次重新计算。上面两种算法中,动态规划的时间复杂度是O(n^2),空间复杂度也是O(n^2)。而brute force的递归算法最坏情况是指数量级的复杂度。 
这种题目在面试中算是比较难的题目,因为分情况比较多,如果不熟悉比较难在面试中理清思路并且做对,我不是很喜欢,但是在面经中却经常看到,所以还是得搞熟悉比较好。类似的题目有Wildcard Matching,那个还更简单一些,不过思路是基本一致的,只是少分一点情况。




class Solution {
public:
    bool matchFirst(const char *s, const char *p){
        return (*p == *s || (*p == '.' && *s != '\0'));
    }

    bool isMatch(const char *s, const char *p) {
        if (*p == '\0') return *s == '\0';  //empty

        if (*(p + 1) != '*') {//without *
            if(!matchFirst(s,p)) 
                return false;
            return isMatch(s + 1, p + 1);
        } 
        else 
        { //next: with a *
            if(isMatch(s, p + 2)) 
                return true;    //try the length of 0
            while ( matchFirst(s,p) )       //try all possible lengths 
                if (isMatch(++s, p + 2))
                    return true;
        }
    }
};

 

class Solution {
public:
    bool isMatch(const char *s, const char *p) {  
        if (*p == '\0') {  
            return *s == '\0';  
        }  
          
        if (*(p + 1) == '*') {  
            while (*s != '\0' && (*s == *p || *p == '.')) {  
                if (isMatch(s, p + 2)) {  
                    return true;  
                }  
                s++;                  
            }  
            return isMatch(s, p + 2);  
        }  
        else {  
            if (*s != '\0' && (*s == *p || *p == '.')) {  
                return isMatch(s + 1, p + 1);  
            }  
        }  
          
        return false;  
    }
};

 

正则表达式匹配,其中.可以表示任意一个字符,*表示零个或者多个前导字符,所谓零个是什么意思呢,给出的例子中,"c*a*b",因为可以表示零个前导 字符,因此第一个星号会吃掉字符c,因此等效于"a*b"。这个是不好理解的。还有就是要求两个字符串必须完全匹配,而不是部分匹配。

下面给出一些个人的分析:

因为星号可能会吃掉前一个字符,因此在进行匹配的时候我们不能逐个字符进行匹配,必须先要看后一个字符是什么,如果不是星号我们尽可以逐个匹配,如果两个字符串的第一个相等,我们递归调用就可以逐次判断。这个是好理解的。

关键在于如果后一个字符是星号,我们应该怎么去比较当前位置,以及怎么去调整两个字符串进行比较,因为星号可以替换成零次或者多次当前 子字符。这又分为两种情况,第一种就是当前字符不匹配,那么很显然,我们会让星号吃掉当前字符,这样就等于是模式串去掉了前两个字符,这个时候递归调用, 即isMatch(s,p+2)。比较难理解的就是当星号前的字符也就是当前字符是匹配的,也会先进行判断isMatch(s,p+2)是否成立,如果成 立说明当前星号也要吃掉前一个字符,如果调用判断返回为真说明后面的是匹配s串的,返回为真即可。如果返回为假说明,当前星号可能代表了多个当前字符,因 此要对s串逐个向后遍历,其中每增加一个说明星号多代表一个当前字符。这样就能理解下面的代码。



posted on 2014-12-28 21:31  风云逸  阅读(108)  评论(0)    收藏  举报