题目:给定一个字符串 (s) 和一个字符模式 (p)。实现支持 '.' 和 '*' 的正则表达式匹配。
'.' 匹配任意单个字符。
'*' 匹配零个或多个前面的元素。
匹配应该覆盖整个字符串 (s) ,而不是部分字符串。
说明:
此博客有提供了两种解法:深搜 和 动态规划。
动态规划解法比较优秀,建议直接看动态规划的解法。
一、深度优先搜索
从题目可知,需要注意的有三个情况:“a”,“.”,“*”(a代表普通字符)
现在假设有 s = abc p = abc
可见,s[0] = a = p[0]。
因为第一个字符匹配,所以可以将问题转化称 s = bc p = bc 的子问题。
所以我们只要解决第一个字符的匹配情况,然后递归匹配其子问题即可。
s与p匹配的时候,s只检查字串的第一个字符即可,而p串则需要检查第1个和第2个字符。
在我的思路中:
p第一个字符可能的情况:“a”、“.”。 下面会说到“*”为什么不可能。
P第二个字符可能的情况:“a”、“.”、“*”。
如果第二个字符是“*”,则说明第一个字符的数量处于 0 - ∞ 的特殊状态。所以需要特殊处理。
即,有两种情况:一、第二个字符不为“*”和第二个字符为“*”。
现在看第一种情况:p第二个字符不为“*”。
这种情况只需要处理第一个字符即可,第二个字符对第一个字符无任何影响。
此时,如果s[0] == p[0] 或 p[0] = ‘.’,则s与p第一个字符匹配。直接将匹配的字符去掉,继续递归匹配子串。
否则,s与p第一个字符不匹配,说明此深搜路线不通。
现在看第二种情况:p第二个字符为“*”
这种情况分三种子情况:
所以,在此处需要引出三个分支:
从上面可知,在a* 的情况下,只要p发生舍弃,肯定就会舍弃a* 这两个字符,所以“*”必定会被附带着舍弃掉,所以只要输入的是合法的表达式,就绝对不会出现第一个字符为 * 的情况。
因为每次递归只处理s第一个字符和p前两个字符,所以简单来说,只要理清下面几种情况的处理方法后,就可以写出这个方法:(xxxx表示任意字符串组合)
S字符串 |
P字符串 |
思路 |
axxxx |
abxxxx |
直接匹配掉第一个字符,处理子串 |
axxxx |
.bxxxx |
同上 |
axxxx |
bbxxxx |
无法匹配,此深搜路线不通,舍弃路线 |
aaxxx |
a*xxxx |
a* 可以匹配掉:空、a、a…。 此时应处理为:匹配空(0)、匹配一次a后a*被匹配掉(1)、匹配一次a后 a* 不被匹配掉(n)。 |
axxxx |
.*xxxx |
.* 同上 |
axxxx |
b*xxxx |
b* 只能匹配掉:空 |
“” |
abxxxx |
无法匹配,此深搜路线不通 |
“” |
a*xxxx |
a* 会匹配掉空, |
axxxx |
“” |
无法匹配,此深搜路线不通 |
“” |
“” |
匹配成功,字符串符合正则表达式(唯一出口) |
可能有遗漏,大家根据情况补全即可。
下面谈谈,这种做法的效率问题:
首先,我们看下面的情况:
S = abxxxx 和 P = a*xxxx
可见,此时P种的 a* 可以匹配为:0 1 n个a。
当匹配为1的时候:S = bxxxx 和 P = xxxx
当匹配为n的时候:S = bxxxx 和 P = a*xxxx。此时,可以在下一次匹配时进一步匹配成:S = bxxxx 和 P = xxxx。
我们看到,S = bxxxx 和 P = xxxx 的情况出现了两次。而且这种重复的情况在执行过程种会大量的出现。大量的重复处理必然导致效率降低。而且是指数形式降低。
1 分为 2 ,2 分为 4 … 重复几次,便成了极为庞大的浪费。
代码:
private boolean subMatch(int sIndex,int pIndex , String s, String p){
int pNextIndex = pIndex + 1;
if(sIndex == s.length() ^ pIndex == p.length()){
if(pNextIndex < p.length() && p.charAt(pNextIndex) == '*'){
return subMatch(sIndex, pIndex + 2, s, p);
}
return false;
}
if(sIndex == s.length() && pIndex == p.length())
return true;
char sChar = s.charAt(sIndex);
char pChar = p.charAt(pIndex);
if(pNextIndex != p.length()){
if(p.charAt(pNextIndex) != '*'){
//下一个字符不是'*';
if(sChar == pChar || pChar == '.'){
//当前字符相等或模式字符为'.',匹配;
if(subMatch(sIndex + 1, pIndex + 1, s, p))
return true;
}else{
//不匹配
return false;
}
} else {
//下一个字符是'*';
if(sChar == pChar || pChar == '.'){
//当前字符相等或模式字符为'.',匹配;
if(subMatch(sIndex + 1, pIndex, s, p))
return true;
if(subMatch(sIndex, pIndex + 2, s, p))
return true;
if(subMatch(sIndex + 1, pIndex + 2, s, p))
return true;
} else {
//不匹配
if(subMatch(sIndex, pIndex + 2, s, p))
return true;
}
}
} else {
if(sChar == pChar || pChar == '.'){
if(subMatch(sIndex + 1, pIndex + 1, s, p)){
return true;
} else {
return false;
}
} else {
return false;
}
}
//this place
return false;
}
public boolean isMatch(String s, String p) {
return subMatch(0,0, s, p);
}
二、动态规划
因为上面思路的大量重复子问题的出现,且每一步的匹配是否成功,均要取决于其子结构是否成功。所以符合动态规划的核心思想:重复子问题 和 最优子结构。
所以只要能列出动态规划的方程,即可很快的码出代码,解决问题。
下面动态方程取自题解中的 powcai 大神:
p[j] == s[i]:dp[i][j] = dp[i-1][j-1]
p[j] == ".":dp[i][j] = dp[i-1][j-1]
p[j] =="*":
3.1 p[j-1] != s[i]:dp[i][j] = dp[i][j-2]
3.2 p[i-1] == s[i] or p[i-1] == ".":
dp[i][j] = dp[i-1][j] //
多个
a
的情况
or dp[i][j] = dp[i][j-1] //
单个
a
的情况
or dp[i][j] = dp[i][j-2] //
没有
a
的情况
dp[i][j]表达的意思是: s的i之前的字串和p的j之前的字串是否匹配。
可以看出,整体分为了三种情况,前两种可以看作一种情况:
如果p[j] == s[i] || p[j] == “.”,则说明匹配,取dp[i][j]=dp[i-1][j-1]。
如果p[j] == “*”,则说明需要用到p[j-1]的字符:
如果p[j-1] != s[i],说明 a* 无法匹配任何字符,取
dp[i][j] = dp[i][j-2](两个字符合起来无法匹配任何字符,所以-2)
如果p[j-1]==s[i] || p[j-1]==“.”。说明a*可以匹配字符,那么dp[i][j]的取值可能来自三个地方:
三个地方,只要有一个为 真 说明就可以匹配。
代码:
public boolean isMatch(String s, String p){
s = "#" + s;
p = "#" + p;
boolean[][] dp = new boolean[s.length() + 1][p.length() + 1];
dp[0][0] = true;
for(int i = 2; i < p.length(); i += 2){
if(p.charAt(i) == '*' && dp[0][i - 2])
dp[0][i] = true;
else
break;
}
for(int i = 1; i < s.length(); i++){
for(int j = 1; j < p.length(); j++){
if(p.charAt(j) != '*'){
if(s.charAt(i) == p.charAt(j) || p.charAt(j) == '.'){
dp[i][j] = dp[i - 1][j - 1];
}
} else {
if(s.charAt(i) == p.charAt(j - 1) || p.charAt(j - 1) == '.'){
dp[i][j] = dp[i][j - 2] | dp[i][j - 1] | dp[i - 1][j];
} else {
dp[i][j] = dp[i][j - 2];
}
}
}
}