请实现一个函数用来匹配包括‘.’
和‘*’
的正则表达式。模式中的字符‘.’
表示任意一个字符,而‘*’
表示它前面的字符可以出现任意次(包含0次)。在本题中,匹配是指字符串的所有字符匹配整个模式。例如,字符串"aaa"
与模式"a.a"
和"ab*ac*a"
匹配,但是与"aa.a"
和"ab\*a"
均不匹配。
每次从字符串里取出一个字符与模式中的字符匹配,如果模式中的字符是‘.’
,它可以匹配字符串中的任意字符,如果不是,那么如果它与字符串中的字符相等则匹配。当字符串的字符和模式的字符匹配时,接着匹配后面的字符。
下面,考虑模式中的第二个字符是不是‘*’
。如果不是,则可以分为两种情况:
false
。当模式中的第二个字符是‘*’
时,又可以分为两种情况方式:
‘*’
和它前面的字符,因为‘*’
可以匹配字符串中的0个字符;"ab"
和模式"a*ab"
匹配到str[0]
和pattern[0]
时,模式后移两个字符,字符串后移一个字符),这一种状态等效于1的情况,可以忽略;"aabbba"
和模式"aab*a"
匹配到str[2]
和pattern[2]
时,模式保持不变,字符串后移一个字符)。根据上一节的分析很容易写出递归方式的代码:
public static boolean match(char[] str, char[] pattern) {
if (str == null || pattern == null) {
return false;
}
return matchCore(str, pattern, 0, 0);
}
private static boolean matchCore(char[] str, char[] pattern, int i, int j) {
//如果模式先遍历完,则一定不匹配;如果同时遍历完,则一定匹配;
if (j == pattern.length) {
return i == str.length;
}
//注意防止数组索引OutOfBounds;
if (j < pattern.length - 1 && pattern[j + 1] == '*') {
//如果模式下一个为'*',且当前匹配;
if (i < str.length && (str[i] == pattern[j] || (pattern[j] == '.'))) {
//模式后移两位,或者模式不变,字符串后移一位;
return matchCore(str, pattern, i, j + 2) || matchCore(str, pattern, i + 1, j);
} else {
//当前不匹配,模式后移两位;
return matchCore(str, pattern, i, j + 2);
}
}
//模式下一个不为'*',且当前匹配,则往后匹配;
if (str.length != i && (str[i] == pattern[j] || pattern[j] == '.')) {
return matchCore(str, pattern, i + 1, j + 1);
}
return false;
}
根据前一小节的递归解法,我们可以分析出动态规划解法。首先看递归代码中的matchCore(str, pattern, i + 1, j + 1)
,其表示整体是否匹配要看下一步是否匹配,递归的过程其实是一种倒序遍历的方式。如果转换成动态规划的思想,并采用顺序遍历的方式,即当前状态是否匹配受上一个状态的影响,上面的代码等效于状态转移方程 d p [ i ] [ j ] = d p [ i − 1 ] [ j − 1 ] dp[i][j]=dp[i-1][j-1] dp[i][j]=dp[i−1][j−1],因此需要建立动态数组boolean dp[str.length+1][pattern.length+1]
,字符串和模式中的字符都从1开始编号,并赋初始值dp[0][0] == true
,表示字符串和模式首端的空字符已匹配。
接下来,分别遍历字符串和模式,检验模式的上一个字符是否为‘*’
,若是,则:
‘*’
前一位字符与字符串的上个字符匹配,则跳过该‘*’
以及它的前一位字符,或者继续看字符串该字符的前一位字符,即 d p [ i ] [ j ] = d p [ i ] [ j − 2 ] ∣ ∣ d p [ i − 1 ] [ j ] dp[i][j]=dp[i][j-2] || dp[i-1][j] dp[i][j]=dp[i][j−2]∣∣dp[i−1][j];‘*’
以及它的前一位字符,即 d p [ i ] [ j ] = d p [ i ] [ j − 2 ] dp[i][j]=dp[i][j-2] dp[i][j]=dp[i][j−2].若模式的上一个字符不是‘*’
,且与字符串的上个字符匹配,则 d p [ i ] [ j ] = d p [ i − 1 ] [ j − 1 ] dp[i][j]= dp[i - 1][j - 1] dp[i][j]=dp[i−1][j−1].
因此,可以写出如下代码:
public static boolean matchDP(char[] str, char[] pattern) {
if (str == null || pattern == null) {
return false;
}
boolean[][] dp = new boolean[str.length + 1][pattern.length + 1];
dp[0][0] = true;
for (int i = 0; i <= str.length; i++) {
for (int j = 1; j <= pattern.length; j++) {
if (j > 1 && pattern[j - 1] == '*') {
if (i > 0 && (str[i - 1] == pattern[j - 2] || pattern[j - 2] == '.')) {
//模式的'*'前一位字符与字符串的上个字符匹配
dp[i][j] = dp[i][j - 2] || dp[i - 1][j];
} else {
//模式的'*'前一位字符与字符串的上个字符不匹配
dp[i][j] = dp[i][j - 2];
}
} else if (i > 0 && (str[i - 1] == pattern[j - 1] || pattern[j - 1] == '.')) {
//模式的前一位字符与字符串的上个字符匹配
dp[i][j] = dp[i - 1][j - 1];
}
}
}
return dp[str.length][pattern.length];
}
public static void main(String[] args) {
char[] str = { 'a', 'a', 'a' };
char[] pattern1 = { 'a', '.', 'a' };
char[] pattern2 = { 'a', 'b', '*', 'a', 'c', '*', 'a' };
char[] pattern3 = { 'a', 'a', '.', 'a' };
char[] pattern4 = { 'a', 'b', '*', 'a' };
//递归方法
System.out.println(match(str, pattern1));
System.out.println(match(str, pattern2));
System.out.println(match(str, pattern3));
System.out.println(match(str, pattern4));
//DP方法
System.out.println(matchDP(str, pattern1));
System.out.println(matchDP(str, pattern2));
System.out.println(matchDP(str, pattern3));
System.out.println(matchDP(str, pattern4));
}