Given an input string (s
) and a pattern (p
), implement wildcard pattern matching with support for '?'
and '*'
.html
'?' Matches any single character. '*' Matches any sequence of characters (including the empty sequence).
The matching should cover the entire input string (not partial).git
Note:github
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:post
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:url
Input: s = "acdcb" p = "a*c?b" Output: false
这道题通配符外卡匹配问题仍是小有难度的,有特殊字符 ‘*’ 和 ‘?’,其中 ‘?’ 能代替任何字符,‘*’ 能代替任何字符串,注意跟另外一道 Regular Expression Matching 正则匹配的题目区分开来。两道题的星号的做用是不一样的,注意对比区分一下。这道题最大的难点,就是对于星号的处理,能够匹配任意字符串,简直像开了挂同样,就是说在星号对应位置以前,无论你s中有任何字符串,我大星号都能匹配你,主角光环啊。但即使叼如斯的星号,也有其处理不了的问题,那就是一旦p中有s中不存在的字符,那么必定没法匹配,由于星号只能增长字符,不能消除字符,再有就是星号一旦肯定了要匹配的字符串,对于星号位置后面的匹配状况也就鞭长莫及了。因此p串中星号的位置很重要,用 jStar 来表示,还有星号匹配到s串中的位置,使用 iStart 来表示,这里 iStar 和 jStar 均初始化为 -1,表示默认状况下是没有星号的。而后再用两个变量i和j分别指向当前s串和p串中遍历到的位置。spa
开始进行匹配,若i小于s串的长度,进行 while 循环。若当前两个字符相等,或着p中的字符是问号,则i和j分别加1。若 p[j] 是星号,要记录星号的位置,jStar 赋为j,此时j再自增1,iStar 赋为i。若当前 p[j] 不是星号,而且不能跟 p[i] 匹配上,此时就要靠星号了,若以前星号没出现过,那么就直接跪,好比 s = "aa" 和 p = "c*",此时 s[0] 和 p[0] 没法匹配,虽然 p[1] 是星号,但仍是跪。若是星号以前出现过,能够强行续一波命,好比 s = "aa" 和 p = "*c",当发现 s[1] 和 p[1] 没法匹配时,可是好在以前 p[0] 出现了星号,把 s[1] 交给 p[0] 的星号去匹配。至于如何知道以前有没有星号,这时就能看出 iStar 的做用了,由于其初始化为 -1,而遇到星号时,其就会被更新为i,只要检测 iStar 的值,就能知道是否可使用星号续命。虽然成功续了命,匹配完了s中的全部字符,可是以后还要检查p串,此时没匹配完的p串里只能剩星号,不能有其余的字符,将连续的星号过滤掉,若是j不等于p的长度,则返回 false,参见代码以下:code
解法一:
class Solution { public: bool isMatch(string s, string p) { int i = 0, j = 0, iStar = -1, jStar = -1, m = s.size(), n = p.size(); while (i < m) { if (j < n && (s[i] == p[j] || p[j] == '?')) { ++i; ++j; } else if (j < n && p[j] == '*') { iStar = i; jStar = j++; } else if (iStar >= 0) { i = ++iStar; j = jStar + 1; } else return false; } while (j < n && p[j] == '*') ++j; return j == n; } };
这道题也能用动态规划 Dynamic Programming 来解,写法跟以前那道题 Regular Expression Matching 很像,可是仍是不同。外卡匹配和正则匹配最大的区别就是在星号的使用规则上,对于正则匹配来讲,星号不能单独存在,前面必需要有一个字符,而星号存在的意义就是代表前面这个字符的个数能够是任意个,包括0个,那么就是说即便前面这个字符并无在s中出现过也无所谓,只要后面的能匹配上就能够了。而外卡匹配就不是这样的,外卡匹配中的星号跟前面的字符没有半毛钱关系,若是前面的字符没有匹配上,那么直接返回 false 了,根本不用管星号。而星号存在的做用是能够表示任意的字符串,固然只是当匹配字符串缺乏一些字符的时候起做用,当匹配字符串p包含目标字符串s中没有的字符时,将没法成功匹配。
对于这种玩字符串的题目,动态规划 Dynamic Programming 是一大神器,由于字符串跟其子串之间的关系十分密切,正好适合 DP 这种靠推导状态转移方程的特性。那么先来定义dp数组吧,使用一个二维 dp 数组,其中 dp[i][j] 表示 s中前i个字符组成的子串和p中前j个字符组成的子串是否能匹配。大小初始化为 (m+1) x (n+1),加1的缘由是要包含 dp[0][0] 的状况,由于若s和p都为空的话,也应该返回 true,因此也要初始化 dp[0][0] 为 true。还须要提早处理的一种状况是,当s为空,p为连续的星号时的状况。因为星号是能够表明空串的,因此只要s为空,那么连续的星号的位置都应该为 true,因此先将连续星号的位置都赋为 true。而后就是推导通常的状态转移方程了,如何更新 dp[i][j],首先处理比较 tricky 的状况,若p中第j个字符是星号,因为星号能够匹配空串,因此若是p中的前 j-1 个字符跟s中前i个字符匹配成功了( dp[i][j-1] 为true)的话,则 dp[i][j] 也能为 true。或者若p中的前j个字符跟s中的前i-1个字符匹配成功了( dp[i-1][j] 为true )的话,则 dp[i][j] 也能为 true(由于星号能够匹配任意字符串,再多加一个任意字符也没问题)。若p中的第j个字符不是星号,对于通常状况,假设已经知道了s中前 i-1 个字符和p中前 j-1 个字符的匹配状况(即 dp[i-1][j-1] ),如今只须要匹配s中的第i个字符跟p中的第j个字符,若两者相等( s[i-1] == p[j-1] ),或者p中的第j个字符是问号( p[j-1] == '?' ),再与上 dp[i-1][j-1] 的值,就能够更新 dp[i][j] 了,参见代码以下:
解法二:
class Solution { public: bool isMatch(string s, string p) { int m = s.size(), n = p.size(); vector<vector<bool>> dp(m + 1, vector<bool>(n + 1, false)); dp[0][0] = true; for (int i = 1; i <= n; ++i) { if (p[i - 1] == '*') dp[0][i] = dp[0][i - 1]; } for (int i = 1; i <= m; ++i) { for (int j = 1; j <= n; ++j) { if (p[j - 1] == '*') { dp[i][j] = dp[i - 1][j] || dp[i][j - 1]; } else { dp[i][j] = (s[i - 1] == p[j - 1] || p[j - 1] == '?') && dp[i - 1][j - 1]; } } } return dp[m][n]; } };
其实这道题也可使用递归来作,由于子串或者子数组这种形式,自然适合利用递归来作。可是愣了吧唧的递归跟暴力搜索并无啥太大的区别,很容易被 OJ 毙掉,好比评论区六楼的那个 naive 的递归,其实彻底是按照题目要求来的。首先判断s串,若为空,那么再看p串,若p为空,则为 true,或者跳过星号,继续调用递归。若s串不为空,且p串为空,则直接 false。若s串和p串均不为空,进行第一个字符的匹配,若相等,或者 p[0] 是问号,则跳过首字符,对后面的子串调用递归。若 p[0] 是星号,先尝试跳过s串的首字符,调用递归,若递归返回 true,则当前返回 true。不然尝试跳过p串的首字符,调用递归,若递归返回 true,则当前返回 true。可是很不幸,内存超出限制了 MLE,那么博主作了个简单的优化,跳过了连续的星号,参见评论区七楼的代码,可是此次时间超出了限制 TLE。博主想是否是取子串 substr() 操做太费时间,且调用递归的适合s串和p串又分别创建了副本,才致使的 TLE。因而想着用坐标变量来代替取子串,而且递归函数调用的s串和p串都加上引用,代码参见评论区八楼,但尼玛仍是跪了,OJ 大佬,刀下留人啊。最后仍是在论坛上找到了一个使用了神奇的剪枝的方法,这种解法的递归函数返回类型不是 bool 型,而是整型,有三种不一样的状态,返回0表示匹配到了s串的末尾,可是未匹配成功;返回1表示未匹配到s串的末尾就失败了;返回2表示成功匹配。那么只有返回值大于1,才表示成功匹配。至于为什么失败的状况要分类,就是为了进行剪枝。在递归函数中,若s串和p串都匹配完成了,返回状态2。若s串匹配完成了,但p串但当前字符不是星号,返回状态0。若s串未匹配完,p串匹配完了,返回状态1。若s串和p串均为匹配完,且当前字符成功匹配的话,对下一个位置调用递归。不然若p串当前字符是星号,首先跳过连续的星号。而后分别让星号匹配空串,一个字符,两个字符,....,直到匹配完整个s串,对每种状况分别调用递归函数,接下来就是最大的亮点了,也是最有用的剪枝,当前返回值为状态0或者2的时候,返回,不然继续遍历。若是仅仅是状态2的时候才返回,就像评论区八楼的代码,会有大量的重复计算,由于当返回值为状态0的时候,已经没有继续循环下去的必要了,很是重要的一刀剪枝,参见代码以下:
解法三:
class Solution { public: bool isMatch(string s, string p) { return helper(s, p, 0, 0) > 1; } int helper(string& s, string& p, int i, int j) { if (i == s.size() && j == p.size()) return 2; if (i == s.size() && p[j] != '*') return 0; if (j == p.size()) return 1; if (s[i] == p[j] || p[j] == '?') { return helper(s, p, i + 1, j + 1); } if (p[j] == '*') { if (j + 1 < p.size() && p[j + 1] == '*') { return helper(s, p, i, j + 1); } for (int k = 0; k <= (int)s.size() - i; ++k) { int res = helper(s, p, i + k, j + 1); if (res == 0 || res == 2) return res; } } return 1; } };
Github 同步地址:
https://github.com/grandyang/leetcode/issues/44
相似题目:
参考资料:
https://leetcode.com/problems/wildcard-matching/
https://leetcode.com/problems/wildcard-matching/discuss/17839/C%2B%2B-recursive-solution-16-ms