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