问题描述
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). The function prototype should be: bool isMatch(const char *s, const char *p) Some examples: isMatch("aa","a") → false isMatch("aa","aa") → true isMatch("aaa","aa") → false isMatch("aa", "*") → true isMatch("aa", "a*") → true isMatch("ab", "?*") → true isMatch("aab", "c*a*b") → false
原问题链接:https://oj.leetcode.com/problems/wildcard-matching/
问题分析
这是一个字符串匹配的问题。看起来和普通的字符串匹配稍微有点不一样的地方就是有两个特殊的字符"?"和"*"。其中"?"表示匹配一个当前的任意字符。而"*"表示匹配当前的若干个字符。如果撇开*号的话,这个问题就挺简单的。因为基本上对于两个串来说,每个字符都是一一对应的,唯一要区分一下的就是"?"这个。当碰到这个字符的时候,让对应位置的字符不管是什么都算匹配就完了。
所以,在不考虑"*"的情况下,我们的方法大致结构如下:
boolean isMatch(String s, String p) { int l = 0, r = 0; while(l < s.length() && r < p.length()) { if(s.charAt(l) == p.charAt(r) || p.charAt(r) == '?') { l++; r++ } else return false; } if(l < s.length() || r < p.length()) return false; return true; }
在前面这部分基础代码思路之上,我们再来考虑一下符号"*"的情况。它的情况之所以比较特殊就是因为当我们的源串s中间某个位置是对应的*号时,这个*号是对应0个或者多个字符的。比如说s = "abaaaac" p = "a*",这个时候p里的*号对应第一个字符a后面的所有部分。如果s="abc", p = "a*bc",这个时候可以说*号对应0个字符。所以,这个时候问题的核心就在于,当s和p在某个位置的时候遇到*号,我们不知道s这边的符号要往前多少个才算是一个合理的匹配。在进一步分析前,我们先看看*号匹配的几种情况:
上图是*号匹配了前面串中间指定点后面所有的字符。下图是匹配了部分串的情况:
对于后面这个匹配了部分串的情况,我们会发现一个有意思的地方。就是从模式串p中*号后面的字符起,如果*后面确实有字符,那么要和前面的串匹配,则前面必然有一部分是和它对应上的。否则这个匹配就失败了。那么我们可以这样来看,碰到*号的时候,取*号后面的那个字符,再去和源字符串比较,如果相等,则尝试往后面继续比较。这种情况可以用下图来描述:
在这种情况下,假定star后面的元素和前面的对应元素匹配上了,我们可以认为这是尝试*号匹配0个元素。可是当我们匹配了若干个之后,却发现如下的情景:
这个时候说明我们*号匹配0个元素并不成立。因为后面的部分没有匹配上。这个时候该怎么办呢?因为我们后面这部分是和前面s串中间刚对应到*号的元素开始的。既然以这个元素为起点和后面的比较不匹配了。我们就尝试它后面的那个元素。所以,后续的比较可以如下:
这一步也是匹配失败,然后按照前面的方式,假定前面的*号匹配了更多的一个,再从后面一个开始尝试:
按照前面的过程,我们一直到最后得到匹配的结果如下:
经过刚才的手工比较过程。我们可以将针对*号的匹配过程总结如下:
1.假设碰到*号时s串的位置是l, p串的位置是r,则将r+1作为*号后续进行比较的起点。
2.从l开始与r+1位置的元素进行比较,如果相同,再按照常规匹配的方式往后面继续比。否则l跳到前面和r+1开始比较那个位置的后面一个。同时p这边的元素也要归位到r+1
所以,到了这一步,我们就知道。当遇到*号的时候,先记录一下l所在的位置和r+1所在的位置。每次匹配比较不对了就回退。假设记录l开始比较位置的元素为match。在每次匹配失败后match就要加1,表示下一个开始比较的起点。
3.既然前面匹配的情况讨论了。对于不匹配的情况呢?假设我们碰到一些不匹配的了。如果前面根本就没有*号,这就是正常的字符串比较不匹配的情况,我们直接就返回匹配不成功了。
4.还有一个问题,这算是比较隐蔽一点的情况。因为我们这边是一碰到*号就一直不停的去比较,如果p串中间有多个*号呢?我们可能已经比较到前面s串的结尾了,但是p串后面可能还有一部分字符。这个时候我们也需要进行判断。如果剩下的是*号,则相当于*号匹配了0个元素,否则表示匹配错误。
基于上述的讨论,一个典型的实现如下:
public class Solution { public boolean isMatch(String str, String pattern) { int s = 0, p = 0, match = 0, starIdx = -1; while (s < str.length()){ // advancing both pointers if (p < pattern.length() && (pattern.charAt(p) == '?' || str.charAt(s) == pattern.charAt(p))){ s++; p++; } // * found, only advancing pattern pointer else if (p < pattern.length() && pattern.charAt(p) == '*'){ starIdx = p; match = s; p++; } // last pattern pointer was *, advancing string pointer else if (starIdx != -1){ p = starIdx + 1; match++; s = match; } //current pattern pointer is not star, last patter pointer was not * //characters do not match else return false; } //check for remaining characters in pattern while (p < pattern.length() && pattern.charAt(p) == '*') p++; return p == pattern.length(); } }
总的来说,基于上述方法来解决这个问题是一种贪婪算法的思路。因为每次碰到一个*号我们会将它记录下来,然后就去和源串进行比较。这种办法效率比较高,当然,只是很难想到。那么除了这个办法,我们还有没有其他方法呢?
其他思路
在我们前面描述解决这个问题的经过时,我们已经发现了一点和递归相关的意思。假设我们两个串,它们在模式串为普通字符或者"?"号的时候,它们两边只要是对应符号相同就表示是匹配的。假设当前两个元素所在的位置分别为l, r。则正好有如下的关系:
if(s[l] == p[r] || p[r] == '?') match(l, r) = match(l + 1, r + 1) 。
这是上面递归关系的一部分。然后对于p[r] == '*'的情况呢?我们知道,需要从s串中间l所在位置往后面每个都去尝试。相当于这样的一个关系:
if(p[r] == '*') {
while(l < s.length()) {
if(match(l, r)) return true;
l++;
}
}
这样我们保证从l位置一直到最后结束部分一直进行比较,直到有一个匹配的。因为这是一个递归的函数定义,它的返回条件则是r == p.length(),表示我们已经遍历到了模式串的结尾了。按照这个思路,得到的代码如下:
boolean isMatch(String s, String p, int l, int r) { if(r == p.length()) return l == s.length(); if(p.charAt(r) == '*') { while(r < p.length() && p.charAt(r) == '*') r++; // Move the index at p to a non-start char. while(l < s.length()) { if(isMatch(s, p, l, r)) return true; // Find one match, return true. l++; // Try the next one. } return isMatch(s, p, l, r); } else if(l < s.length() && (p.charAt(r) == '?' || s.charAt(l) == p.charAt(r))) return isMatch(s, p, l + 1, r + 1); return false; }
通过这种递归的关系我们得到的这个算法虽然定义清楚了其中的关系,但是如果去分析它的时间复杂度的话会发现这是一个指数级别的,在实际中执行起来并不理想。那么,有没有办法可以改进一下呢?
改进
我们知道前面的问题在于我们虽然得出了递归的关系,但是执行的效率偏低。这个时候我们可能会想到一个点,就是通常递归的方法执行比较慢的时候,我们去看看它们的递归的关系。是否存在递归过程中对一些结果的重复计算。如果有的话,这就是一个利用动态规划方法的地方。
假设我们用F(i, j)表示串s[0...i], p[0...j]这两个串分别到i和j位置它们是否匹配的函数。那么,我们可以得到这样的一个递推关系:
F[i, j] = F[i-1, j-1] &&(s[i] == p[j] || p[j] == '?') (假设此时p[j] != '*')
F[i, j] = or F[i-1, j-1] for i = 0, ... i-1, 如果p[j] = '*'。这里表示我们从前面开始找到一个对应j-1时为true的情况,表示对应i-1的时候s串匹配的位置,则从这个位置后面所有的位置都是true。
所以按照前面的思路,我们可以定义一个二维数组boolean[][] dp = new boolean[s.length()][p.length()]; 参考的实现如下:
public class Solution { public boolean isMatch(String s, String p) { int m = s.length(); int n = p.length(); boolean[][] dp = new boolean[m + 1][n + 1]; dp[0][0] = true; for (int j = 1; j <= n; j++) dp[0][j] = dp[0][j - 1] && p.charAt(j - 1) == '*'; for(int i = 1; i <= m; i++) { for(int j = 1; j <= n; j++) { if(p.charAt(j - 1) == s.charAt(i - 1) || p.charAt(j - 1) == '?') dp[i][j] = dp[i - 1][j - 1]; else if(p.charAt(j - 1) == '*') dp[i][j] = dp[i - 1][j] || dp[i][j - 1]; } } return dp[m][n]; } }
这种实现方式相对来说性能有了很大的提升,它的时间复杂度为o(m * n)。空间的复杂度也是O(m * n)。实际上,我们还可以对代码做进一步的改进。因为空间计算每次只是用到前一次计算的结果,我们可以将空间的复杂度降低到O(m)。具体的实现这里就不再赘述了。
总结
对于字符串的模糊匹配问题看起来很简单,实际上牵涉到非常复杂的判断条件。结合不同的场景,最关键的就是要找到它们的递归关系,然后进行动态规划方法的优化。
参考材料
http://yucoding.blogspot.com/2013/02/leetcode-question-123-wildcard-matching.html
http://www.cnblogs.com/codingmylife/archive/2012/10/05/2712585.html
http://tech-wonderland.net/blog/leetcode-wildcard-matching.html
http://blog.csdn.net/linhuanmars/article/details/21198049
相关推荐
wildcard-matchingpackage io.lcalmsky.leetcode. wildcard_matchingclass io.lcalmsky.leetcode. wildcard_matching.Solutiontest io.lcalmsky.leetcode. wildcard_matching.SolutionTest问题清
Wildcard Matching Wildcard Matching O(N) by Jimbowhy http://blog.csdn.net/WinsenJiansbomber/article/details/50862569 在 LeetCode 上看到第二个有趣的问题,是关于字符串匹配的,在接触过正则表达式后就...
wildcard matching 动态规划 longest common prefix , 简单 valid number, hard, 用有限自动机 integer to roman ,easy , 模拟 roman to integer ,easy , 模拟 count and say , easy , 模拟 Anagrams 字符串处理,map...
leetcode 答案 算法心得 大纲 解算法 = 思路->思路验证->直译->结果验证 进步 = 解算法->看高手答案->临摹->形成后续TODO ...容易跑偏,要直译,要直译,要直译!...很多都是可以直接求出来的...No.44(wildcard matching) No
js js_leetcode题解之44-wildcard-matching.js
力码解决方案 Leetcode是一个网站,人们——主要是软件工程师——练习他们的编码技能。 有 800 多个问题(并且还在不断...├── Wildcard Matching │ ├── Readme.md │ └── solution.js ├── Valid Number │
leetcode中国 买卖股票 从easy一直问到hard 数独 从medium的判断是不是valid 到 hard的solve 数独 从BST里面删除一个node (要求写test case) design就是皮毛的问题。。。我几乎就是把Harvard那个intro system design...
- Wildcard Matching: 实现支持'?'和'*'的通配符匹配。 - Jump Game / Jump Game II: 第一个问题要求判断是否能到达数组的最后一个位置,第二个问题要求求出最小跳跃次数到达最后一个位置。 - Permutations / ...
例如,“Wildcard Matching”和“Valid Palindrome II”这类题目要求考虑通配符或特殊字符的存在,增加了问题的复杂性。在处理这类问题时,了解API的使用、忽略字符大小写和字符编码差异是十分重要的。 最后,树...
- **通配符匹配(Wildcard Matching)**: 实现通配符‘*’和‘?’匹配。 #### 数组操作 - **查找字符串中的重复元素(Remove Duplicates)**: 从排序数组中删除重复项。 - **两数之和(2 Sum)**: 给定一个整数数组 nums ...
首先,让我们来看一下“Problem1: Wildcard Matching”。这个问题的核心是实现一个函数,它接受两个参数:一个字符串`s`和一个包含通配符`*`的模式`p`,并判断`s`是否能通过重新排列字符匹配`p`。通配符`*`在这里...
- Wildcard Matching(通配符匹配) - Length of Last Word(最后一个单词的长度) - Count and Say(猜数字序列) - **Integer Array**(整型数组操作) - Remove Element(移除元素) - Zero Sum Subarray...
Wildcard Matching Python 2016/4/1 Hard 051 N-Queens C++ 2016/5/18 Hard 092 Reverse Linked List II Python 2017/11/28 Medium 095 Unique Binary Search Trees Python 2017/11/28 Medium 09
9 Wildcard Matching 37 10 Regular Expression Matching in Java 39 11 Merge Intervals 43 12 Insert Interval 45 13 Two Sum 47 14 Two Sum II Input array is sorted 49 15 Two Sum III Data structure design ...