diff --git a/README.md b/README.md index 9c94b57..ea67920 100644 --- a/README.md +++ b/README.md @@ -46,6 +46,7 @@ My LeetCode solutions with Chinese explanation. 我的LeetCode中文题解。 | 41 |[First Missing Positive](https://leetcode.com/problems/first-missing-positive/)|[C++](solutions/41.%20First%20Missing%20Positive.md)|Hard| | | 42 |[Trapping Rain Water](https://leetcode.com/problems/trapping-rain-water/)|[C++](solutions/42.%20Trapping%20Rain%20Water.md)|Hard| | | 43 |[Multiply Strings](https://leetcode.com/problems/multiply-strings/)|[C++](https://github.com/ShusenTang/LeetCode/blob/master/solutions/43.%20Multiply%20Strings.md)|Medium| | +| 44 |[Wildcard Matching](https://leetcode.com/problems/wildcard-matching/)|[C++](solutions/44.%20Wildcard%20Matching.md)|Hard| | | 46 |[Permutations](https://leetcode.com/problems/permutations/)|[C++](https://github.com/ShusenTang/LeetCode/blob/master/solutions/46.%20Permutations.md)|Medium| | | 47 |[Permutations II](https://leetcode.com/problems/permutations-ii/)|[C++](https://github.com/ShusenTang/LeetCode/blob/master/solutions/47.%20Permutations%20II.md)|Medium| | | 48 |[Rotate Image](https://leetcode.com/problems/rotate-image/)|[C++](https://github.com/ShusenTang/LeetCode/blob/master/solutions/48.%20Rotate%20Image.md)|Medium| | diff --git a/solutions/44. Wildcard Matching.md b/solutions/44. Wildcard Matching.md new file mode 100644 index 0000000..ef83d64 --- /dev/null +++ b/solutions/44. Wildcard Matching.md @@ -0,0 +1,168 @@ +# [44. Wildcard Matching](https://leetcode.com/problems/wildcard-matching/) + +# 思路 + +通配符匹配问题。 + +* '?'匹配任意非空字符; +* '*'匹配任意字符串(包括空串)。 + +注意和 10. Regular Expression Matching 正则匹配的题目区分开来。 + +## 思路一、递归+剪枝 + +此题最容易想到的就是递归的思路,但是naive的递归很容易超时(即使是加入了记忆cache和跳过连续的星号)。超时的原因是存在了大量不必要的调用(即不可能返回true但我们还是调用了),为了避免这个问题,我们定义一个返回值有三种情况的递归函数: + +* 0:s串可被匹配完成,但p有多余字符(非'*'),如 s=abc,p=abcd; +* 1:未匹配到s串的末尾就失败了; +* 2:成功匹配。 + +这样表示的目的是为了剪枝。我们来看看递归函数: + +* 递归出口: + * 若p为空,如果s也为空当然返回2,而如果s不为空返回则 1; + * 否则,若s为空且`p[0] != '*'`则返回0; +* 否则,我们如果第一个字符能匹配上,即`s[i] == p[j] || p[j] == '?'`,那返回递归调用后面字符匹配结果即可。 +* 否则,要匹配只能寄希望于'*'了,所以如果`p[0] == '*'`,我们首先要跳过连续的星号,因为连续的星号和一个星号是完全等价的;然后我们用一个for循环分别让星号匹配空串、s的第一个字符、s的前两个字符、...及整个s串,对每种情况分别调用递归函数,这个过程就需要最重要的剪枝了: + * 若某次递归调用的返回值为2,即可返回2,函数结束,这很好理解。 + * 若某次递归调用的返回值为0,这说明此时s的剩余字符太少了(或者说 p 的剩余字符太多了),我设此时星号匹配s的前k个字符,那如果星号匹配的是s的前k+1个字符,那么s的剩余字符会比现在还少一个,因此接下来的情况都只可能返回0。因此,这种情况我们直接返回0即可,函数结束。 + +* 否则,那么匹配失败,返回1。 + +时间亲测8ms + +## 思路二、动归 + +状态定义为 +``` +dp[i][j] = true 表示 s[0,1,..,i-1]和p[0,1,..,j-1]]能匹配上 +``` +需要仔细考虑一下初始状态,首先我们知道两个空串是能匹配的,所以`dp[0][0]=true`,另外由于星号可匹配空串,所以如果`p[k] = '*'`,我们需要将`dp[0][k]初始化为``,...,k-1]`都是true。 + +接下来就是状态转移方程,根据是否是星号有两种情况: +1. `p[j-1] == '*'`,那么p[j-1]既可以匹配掉当前s的字符s[i-1]也可以选择不匹配(即匹配空字符),即`p[i][j] = dp[i - 1][j] || dp[i][j - 1]`; +2. 否则,就只能看p[j-1]和s[i-1]是否能够硬性匹配了,而且还有考虑前面的匹配情况,即`dp[i][j] = (s[i - 1] == p[j - 1] || p[j - 1] == '?') && dp[i - 1][j - 1]`。 + +可见状态数组里的`dp[i][j]`只和其上(`dp[i - 1][j]`)、左上(`dp[i - 1][j - 1]`)、左(dp[i][j - 1])边的元素有关,所以我们可以考虑用滚动数组优化空间,以及剪枝(若上一行元素和当前行第一个元素dp[i][0]都为false的话直接返回false就可以了)。 + +时间复杂度O(mn)(亲测96ms),空间复杂度可优化至线性。 + +## 思路三、0ms + +此题还有一个比较难想但是亲测最快的方法。 + +基本的思路就是贪婪地一一匹配,如果在某处匹配失败了,那么回到最近的星号处,将本次该星号匹配的字符数加上1继续匹配,即若本次该星号匹配了k个字符,那么再多匹配一个字符匹配k+1个字符重新尝试匹配。 + +为此我们用jStar来表示p串中星号的位置,还有用iStar表示星号匹配到s串中的最后位置,iStar和jStar均初始化为 -1,表示还没遇到星号。另外设置两个工作指针i和j分别指向s和p的当前字符,初始均为0。 + +具体匹配过程: + +while循环直到 i 等于s串的长度, + +* 如果`j < n && (s[i] == p[j] || p[j] == '?')`,说明硬匹配成功,两个工作指针后移即可; +* 否则,如果`j < n && p[j] == '*'`,表明遇到了最新的星号,需要更新iStar和jStar,`istar = i; jstar = j;`,然后我们后移工作指针 j,但是不移 i 代表这个星号匹配了0个字符,所以下次还要从当前字符开始。 +* 否则,表示当前匹配失败了,那唯一的希望就是回到最近匹配的星号处(所以前提是`istar >= 0`),让这个星号多匹配一个字符然后再重新向后匹配,即`i = ++istar; j = jstar + 1`。 +* 否则,唯一的希望也破灭了,只能返回false,函数结束。 + +while循环结束。此时s串已匹配完毕,如果p串也匹配完毕或者剩下的全是星号则说明匹配成功,否则匹配失败。 + +时间复杂度最坏O(mn),但是亲测是最快的(0ms),空间复杂度O(1) + + +# C++ +## 思路一 +``` C++ +class Solution { +private: + int sn, pn; // s.size, p.size + int helper(string& s, string& p, int i, int j) { + /* + 0: s被匹配完成但p有多余字符(非*), 如s = ab, p = abc + 1: 未匹配到s串的末尾就失败了 + 2: 成功匹配 + */ + if(j == pn) return i == sn ? 2 : 1; + if (i == sn && p[j] != '*') return 0; + + if (s[i] == p[j] || p[j] == '?') + return helper(s, p, i + 1, j + 1); + + if (p[j] == '*') { + while(j+1 < pn && p[j+1] == '*') j++; // 跳过连续星号 + + for (int k = 0; k <= sn - i; ++k) { + int res = helper(s, p, i + k, j + 1); + if (res != 1 ) return res; // res = 0 or 2, 剪枝 + } + } + return 1; + } +public: + bool isMatch(string s, string p) { + sn = s.size(); pn = p.size(); + return helper(s, p, 0, 0) > 1; + } + +}; +``` + +## 思路二 +``` C++ +class Solution { +public: + bool isMatch(string s, string p) { + int m = s.size(), n = p.size(); + vector> dp(m + 1, vector(n + 1, false)); + dp[0][0] = true; + + for (int k = 1; k <= n; ++k) + if (p[k - 1] == '*') dp[0][k] = true; + else break; + + bool pre_row_exist_true = true; + for (int i = 1; i <= m; ++i) { + // 若上一行和当前行的第一个元素都为false的话直接返回false + if(!pre_row_exist_true && !dp[i][0]) return false; + pre_row_exist_true = dp[i][0]; + 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]; + pre_row_exist_true = pre_row_exist_true || dp[i][j]; + } + } + return dp[m][n]; + } +}; +``` +## 思路三 +``` C++ +class Solution { +public: + bool isMatch(string s, string p) { + if(p.empty()) return s.empty(); + + int m = s.size(), n = p.size(), i = 0, j = 0; + int istar = -1, jstar = -1; + 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; + } +}; +``` \ No newline at end of file