Given an input string (s) and a pattern (p), implement wildcard pattern matching with support for '?' and '*'.

'?' Matches any single character.
'*' Matches any sequence of characters (including the empty sequence).

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

Note:

  • s could be empty and contains only lowercase letters a-z.
  • p could be empty and contains only lowercase letters a-z, and characters like ? or *.

Example 1:

Input:
s = "aa"
p = "a"
Output: false
Explanation: "a" does not match the entire string "aa".

Example 2:

Input:
s = "aa"
p = "*"
Output: true
Explanation: '*' matches any sequence.

Example 3:

Input:
s = "cb"
p = "?a"
Output: false
Explanation: '?' matches 'c', but the second letter is 'a', which does not match 'b'.

Example 4:

Input:
s = "adceb"
p = "*a*b"
Output: true
Explanation: The first '*' matches the empty sequence, while the second '*' matches the substring "dce".

Example 5:

Input:
s = "acdcb"
p = "a*c?b"
Output: false

 

正则匹配问题其实是一种非确定有限状态自动机的构建。本题是通配符匹配,但和正则匹配很相似。只要能构建出来这道题就解决了。尝试构建之前先看看自动机是如何模拟正则匹配的。首先要明确特殊字符'?'与'*'分别代表了什么。'?'很好理解,即它可以和任何单个非空字符匹配。而'*'可以和任何字符串(包括空串)匹配。也就是说,只要'*'出现,那么它既可以使自动机向后运行,即创造了向后的通路;也可以使自动机的下一个状态回到它所在的位置,即创造了后一个节点回到其本身的通路。因为它可以匹配任何字符串,即当需要的时候,它可以“吸收”一切使得自动机停滞不前的字符。用第四个例子举例,构建的p串自动机如下:

这里人为添加一个实心节点作为终点。如果s全被扫描,且p最后到达了终点,即是匹配成功。模拟自动机之前,要清楚自动机总是向着匹配成功的目标运行,即要优先考虑匹配,才去向下移动二者的指针。如果不匹配,再考虑二者有没有因为'*'产生的向后或者向前的通路。而且如果s串由于不匹配而无法前进的状态只能停留一次。这里要特别解释一下这一点:如果s串遇到了一个字符,同时p串遇到了'*',尽管i此时可以向下移动(因为p对应的是'*'),也可以不移动,那么本着匹配优先向下移动的原则,i是此时应该优先停在原地,只有j可以向下移动。而如果p的下一个字符依然不能和s的字符匹配,那么p的指针j要沿着由于'*'产生的向前通路回到'*'处,而i已经上轮已经停留在此了,所以这次不能再本着匹配才能向下移动的原则留在此地了,毕竟它不是无路可走,它可以向下。如果它不想下,就会产生死循环。说的很拗口,还是举例用例子4来说,新建两个指针i和j,i和j分别指向s串与p串自动机的第一个字符,模拟如下:

1. i -> 'a' / j -> '*' :不匹配发生,因为p串指向的字符是'*',所以s与p同时产生向下的通路,但由于这是第一次i到这个位置,本着匹配优先的原则,i留在原地,只有j后移。且同样由于此时字符是'*',产生下一个字符回溯到当前位置的通路,如图所示;

2. i -> 'a' / j -> 'a':找到匹配,s串和p串指针同时向后移动;

3. i -> 'd' / j -> '*':不匹配发生,因为p串指向的字符是'*',所以s与p同时产生向下的通路,但由于这是第一次i到这个位置,本着匹配优先的原则,i留在原地,只有j后移。且同样由于此时字符是'*',产生下一个字符回溯到当前位置的通路,如图所示;

4. i -> 'd' / j -> 'b':不匹配发生,因为不匹配,且p指向的不是'*',所以i无法向后走,j亦无法向后,为了保证自动机运行,j只能沿着之前产生的通路回到之前的位置,即j回到第二个'*'处;

5. i -> 'd' / j -> '*':不匹配发生,因为p串指向的字符是'*',所以s与p同时产生向下的通路,此时这已经是i第二次走到这个位置了,所以i必须向后运行才可以避免死循环,所以i后移。j亦后移(其实j可以不后移,但如果j停留在'*',那么下一步一定是i指向的字符又和'*'不匹配而导致j自己后移,所以此时不如直接将j后移到'*'的下一个位置,进而省略一步);

6. i -> 'c' / j -> 'b':此时情况和第4步一样,j先向回走,i因为不是匹配留在原地一次,进而下一步的时候需要和j同时后移。所以最后的结果是和经历和第5步一样状态(i -> 'c' / j -> '*')后,i和j同时向后,即i从'c'移动到最后一个字符'b',而j从'*'向后重新移动到'b';

7. i -> 'b' / j -> 'b':找到匹配,s串和p串指针同时向右移动。此时p已经移动到实心终点,而s亦全部扫描完成,返回匹配true。

模拟出来自动机运行,证明了这个就是可以寻找匹配的过程,那么下面就要开始构建自动机:

首先本着优先匹配后移的原则,如果s[i] == p[j],那么i和j同时后移。这里可以把字符'?'加进去,因为遇到'?'和遇到匹配的情况完全一样,所以s[i] == p[j] || p[j] == '?',i与j后移;

如果不匹配,但j指向'*',那么要做三件事:1) 构建p的回路;2) 标记此刻i因为不匹配优先而停留的位置,以便i下次不会有路走而不走;3) 后移j。这时就要引入两个变量,一个来存储此时j指向的位置,用来构建回路。一个来记录i的位置,用来提醒i下次不要有路走而不走。即,pStar = j;sStar = i;j++;

同样如果不匹配,但是i并不是无路可走(即sStar存在),那么i走到sStar的下一个位置。由于sStar存在,pStar一定存在(因为二者同时建立),那么j此刻要到pStar+1的位置(理由见步骤5的括号中解释);

如果i与j不匹配还都无路可走,那么只能返回false,即s和p无论如何不能匹配。

构建自动机的过程亦是完成代码的过程,详见下文代码解释。

 


Java

class Solution {
    public boolean isMatch(String s, String p) {
        char[] S = s.toCharArray(), P = p.toCharArray();
        int i = 0, j = 0, sStar = -1, pStar = -1;
        while (i < s.length()) {
            if (j < p.length() && (S[i] == P[j] || P[j] == '?')) { //如果匹配,两指针同时后移
                i++;
                j++;
            }
            else if (j < p.length() && P[j] == '*') { //如果不匹配但j指向'*',那么记录此时j的位置以构建回路,同时记录i的位置以标记i此时可以后移却停留在此一次,同时j后移
                pStar = j++;
                sStar = i;
            }
            else if (sStar >= 0) { //仍然不匹配,但是i有路可走,且i已经停在那一次了,那么i要后移,连同i停留的位置也要更新,j直接到回路'*'的后一个位置。此时j也可以取pStar,但运行速度会变慢
                j = pStar + 1;
                i = ++sStar;
            }
            else return false; //仍然不匹配,i与j均已无路可走,返回false
        }
        while (j < p.length() && P[j] == '*') j++; //i扫描完成后要看j能不能够到达终点,即j可以沿着'*'行程的通路一直向下
        return j == p.length(); //i与j同时到达终点完成匹配
    }
}

 

posted on 2018-07-14 20:31  小T在学习  阅读(515)  评论(1编辑  收藏  举报