Implement regular expression matching with support for '.' and '*'.
'.' Matches any single character. '*' Matches zero or more of the
preceding element.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", "a*") → true
isMatch("aa", ".*") → true
isMatch("ab", ".*") → true
isMatch("aab", "c*a*b") → true
难度: Hard
这道题要求我们实现简单的正则表达式的匹配, 只要求普通字符 . *的匹配, 了解正则的同学都清楚, .代表任意单个字符, *代表0个或多个前面的字符, 比如a*可以匹配到空字符串, 也可以匹配 a, aaa等等. 题目还要求, 我们判定正则是否匹配给定的字符串, 要判定整个字符串, 而不是其中一部分匹配就算ok.
这是个典型的动态规划的题, 作者在leetcode出来之前几乎不做算法, 本着死磕到底不看答案不看别人解答的精神, 我尝试了不止一种解法, 这里贴出两个AC的解法.
任意确定字符, 必须精确匹配, . 匹配一个任意字符, 这些都很好判定, 关键在于, * 到底匹配多少个字符? 这就不好判定了, 只能根据一定规则去尝试, 这里就是用到动态规划的地方.
第一个AC的解法主要思路为:
切分正则的pattern, 将带*号的都切开, 比如.aa* 可以切为 .~a~a* 三段
使用一个栈, 对不同的可能性(这些可能性实际上形成一个搜索树)做深度优先搜索(也可以不用栈, 直接递归, 我的代码里没有展示)
对于不带*的分段, 直接严格匹配
对于带*的分段, 如果跟当前游标处的字符相同, 则尝试匹配一个字符(下次判定还可以匹配一个, 这样实际可以做到匹配多个), 或者匹配0个(也就是将当前子pattern分段抛弃); 如果跟当前游标处的字符不同, 则只能匹配0个, 将当前子pattern分段抛弃.
代码如下
public class Solution2 {
public class Symbol {
public Symbol() {
rep = false;
}
public Symbol(char f, boolean s) {
c = f;
rep = s;
}
public char c;
public boolean rep;
}
public class Pair<T> {
public T first;
public T second;
public Pair() {
first = null;
second = null;
}
public Pair(T _f, T _s) {
first = _f;
second = _s;
}
}
/**
* AC, but slow
*/
public boolean isMatch(String s, String p) {
// parse pattern
List<Symbol> pl = new ArrayList<Symbol>();
int i = 0;
while (i < p.length()) {
char c;
boolean rep = false;
char a = p.charAt(i);
if (a != '*') {
c = a;
} else {
// regex wrong
return false;
}
i++;
if (i < p.length() && p.charAt(i) == '*') {
rep = true;
i++;
}
pl.add(new Symbol(c, rep));
}
// do match
Stack<Pair<Integer>> q = new Stack<Pair<Integer>>();
q.push(new Pair<Integer>(0, 0));
while (!q.isEmpty()) {
Pair<Integer> pr = q.pop();
if (isMatch(s, pr.first, pl, pr.second, q)) {
return true;
}
}
return false;
}
private boolean isMatch(String s, int sPos, List<Symbol> pl, int pPos, Stack<Pair<Integer>> q) {
while (sPos < s.length() && pPos < pl.size()) {
Symbol sym = pl.get(pPos);
if (sym.rep) {
if (sym.c == '.' || sym.c == s.charAt(sPos)) {
q.push(new Pair<Integer>(sPos, pPos + 1));
q.push(new Pair<Integer>(sPos + 1, pPos));
} else {
q.push(new Pair<Integer>(sPos, pPos + 1));
}
return false;
} else {
if (sym.c != s.charAt(sPos) && sym.c != '.') {
return false;
}
}
sPos++;
pPos++;
}
if (sPos < s.length()) {
return false;
}
if (pPos < pl.size()) {
while (pPos < pl.size()) {
if (!pl.get(pPos).rep) {
return false;
}
pPos++;
}
}
return true;
}
public static void main(String[] args) {
Solution2 s = new Solution2();
System.out.println(s.isMatch("aa", "a"));
System.out.println(s.isMatch("aa", "aa"));
System.out.println(s.isMatch("aaa", "aa"));
System.out.println(s.isMatch("aa", "a*"));
System.out.println(s.isMatch("aa", ".*"));
System.out.println(s.isMatch("aab", "c*a*b"));
System.out.println(s.isMatch("ab", ".*"));
System.out.println(s.isMatch("aaab", ".*ab"));
System.out.println(s.isMatch("aaa", "a*a"));
System.out.println(s.isMatch("aaa", "aaab*"));
System.out.println(s.isMatch("bbab", "b*"));
System.out.println(s.isMatch("bbab", "a*"));
System.out.println(s.isMatch("bbab", "b*a*"));
System.out.println(s.isMatch("bbab", "...."));
System.out.println(s.isMatch("abbabaaaaaaacaa", "a*.*b.a.*c*b*a*c*"));
System.out.println(s.isMatch("bbabaaaaaaacaa", "b.a.*c*b*a*c*"));
System.out.println(s.isMatch("baaaaaaacaa", ".*c*b*a*c*"));
System.out.println(s.isMatch("caa", "c*b*a*c*"));
System.out.println(s.isMatch("caa", "c*b*a*"));
System.out.println(s.isMatch("a", "a*b*"));
System.out.println(s.isMatch("a", "a*.*"));
System.out.println(s.isMatch("ab", "a*b"));
System.out.println(s.isMatch("b", ".*b"));
System.out.println(s.isMatch("ab", ".*b"));
System.out.println(s.isMatch("aaaaaaaaaaaaab", "a*a*a*a*a*a*a*a*a*a*a*a*b"));
}
}
main
中包含了测试用例.
这个解法虽然AC了, 但是存在以下问题:
执行速度较慢, 只超过了百分之几的成功提交.(直接改递归在java中会更快的)
代码思路不是很清晰
这种动态规划比较保守, 判定较长, 搜索的树比较深.
于是尝试了第二个解法, 跟第一个很不同, 主要思路为:
首先还是切分正则的pattern, 将带*号的都切开, 比如.aa* 可以切为 .~a~a* 三段. 不过图方便直接切成多个string即可, 其实也有其他的存储方案.
从前/后两个方向, 遍历子pattern列表, 把不重复(不带*)的部分, 跟目标字符串的相应位置做严格匹配, 直到遇到带*的子pattern. 此时匹配不成功可以认为正则匹配失败.
剩下的子pattern列表中, 如果包含不带*号的子pattern, 则寻找所有在目标字符串中能匹配到的字符, 对每个这样的字符, 把字符串和子pattern列表切分成两段, 看这两段是否可以匹配, 当且仅当这两段都匹配成功, 才算整个字符串匹配正则成功.
剩下的子pattern列表中, 如果不包含不带*号的子pattern, 即全部都是带*的模糊匹配. 我们就采用贪心算法, 对每个子pattern, 匹配尽量多的字符, 如果能把当前字符串匹配干净, 就算ok.
public class Solution3 {
/**
* AC, fast enough
*/
public boolean isMatch(String s, String p) {
List<String> plist = new ArrayList<String>();
int pi = 0;
while (pi < p.length()) {
if (p.charAt(pi) == '*') {
// regex wrong
return false;
}
if (pi + 1 < p.length() && p.charAt(pi + 1) == '*') {
plist.add(p.substring(pi, pi + 2));
pi += 2;
} else {
plist.add(p.substring(pi, pi + 1));
pi += 1;
}
}
return isMatch(s, 0, s.length(), plist, 0, plist.size());
}
/**
*
* @param s string to be matched
* @param ss start position of s (inclusive)
* @param se end position of s (exclusive)
* @param plist pattern list
* @param ps start position of plist (inclusive)
* @param pe end position of plist (exclusive)
* @return
*/
public boolean isMatch(String s, int ss, int se, List<String> plist, int ps, int pe) {
if (ps == pe) {
if (ss != se) {
return false;
} else {
return true;
}
}
while (ps < pe && plist.get(ps).length() == 1 && ss < se) {
char c = plist.get(ps).charAt(0);
if (c != '.' && c != s.charAt(ss)) {
return false;
}
ss++;
ps++;
}
while (ps < pe && plist.get(pe - 1).length() == 1 && ss < se) {
char c = plist.get(pe - 1).charAt(0);
if (c != '.' && c != s.charAt(se - 1)) {
return false;
}
se--;
pe--;
}
if (ps == pe && ss == se) {
return true;
}
if (ps == pe) {
return false;
}
if (ss == se) {
for (int i = ps; i < pe; i++) {
if (plist.get(i).length() == 1) {
return false;
}
}
return true;
}
// select one single sub-pattern
int pi = 0;
for (pi = ps; pi < pe; pi++) {
if (plist.get(pi).length() == 1) {
break;
}
}
if (pi < pe) {
// found single sub-pattern
char c = plist.get(pi).charAt(0);
for (int si = ss; si < se; si++) {
if (c == '.' || s.charAt(si) == c) {
boolean b1 = isMatch(s, ss, si, plist, ps, pi);
if (!b1) {
// early termination
continue;
}
boolean b2 = isMatch(s, si + 1, se, plist, pi + 1, pe);
if (b2) {
return true;
}
}
}
return false;
}
// single sub-pattern not found, all * patterns
// do greedy
for (pi = ps; pi < pe; pi++) {
char c = plist.get(pi).charAt(0);
if (c == '.') {
// will consume all
return true;
}
while (ss < se && (s.charAt(ss) == c)) {
ss++;
}
if (ss == se) {
return true;
}
}
return false;
}
public static void main(String[] args) {
Solution3 s = new Solution3();
System.out.println(s.isMatch("aa", "a"));
System.out.println(s.isMatch("aa", "aa"));
System.out.println(s.isMatch("aaa", "aa"));
System.out.println(s.isMatch("aa", "a*"));
System.out.println(s.isMatch("aa", ".*"));
System.out.println(s.isMatch("aab", "c*a*b"));
System.out.println(s.isMatch("ab", ".*"));
System.out.println(s.isMatch("aaab", ".*ab"));
System.out.println(s.isMatch("aaa", "a*a"));
System.out.println(s.isMatch("aaa", "aaab*"));
System.out.println(s.isMatch("bbab", "b*"));
System.out.println(s.isMatch("bbab", "a*"));
System.out.println(s.isMatch("bbab", "b*a*"));
System.out.println(s.isMatch("bbab", "...."));
System.out.println(s.isMatch("abbabaaaaaaacaa", "a*.*b.a.*c*b*a*c*"));
System.out.println(s.isMatch("bbabaaaaaaacaa", "b.a.*c*b*a*c*"));
System.out.println(s.isMatch("baaaaaaacaa", ".*c*b*a*c*"));
System.out.println(s.isMatch("caa", "c*b*a*c*"));
System.out.println(s.isMatch("caa", "c*b*a*"));
System.out.println(s.isMatch("a", "a*b*"));
System.out.println(s.isMatch("a", "a*.*"));
System.out.println(s.isMatch("ab", "a*b"));
System.out.println(s.isMatch("b", ".*b"));
System.out.println(s.isMatch("ab", ".*b"));
System.out.println(s.isMatch("aaaaaaaaaaaaab", "a*a*a*a*a*a*a*a*a*a*a*a*b"));
}
}
main
中包含了测试用例.
这个解法看代码直觉就可以感到搜索深度会相对于上个解法小很多, 因为能在本次判定中完成的, 就在本次判定中完成, 不放到下次. 实际也可以超过75%的提交, 并且思路相对清晰的多, 虽然代码长了点.
可以看到, 对于动态规划的问题, 需要做的是:
确定的部分尽快匹配, 给出答案
不确定的部分, 单独切分出来, 使用动态规划
以上是我的解法, 不知道是否有更好更清晰的解.
另: 这道题跟 Leetcode 44 通配符匹配很相似, 稍后给出Leetcode 44的解.
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。