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 lettersa-z
.p
could be empty and contains only lowercase lettersa-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同时到达终点完成匹配 } }