题目
给你一个字符串 s 和一个字符规律 p,请你来实现一个支持 '.' 和 '*' 的正则表达式匹配。
'.' 匹配任意单个字符
'*' 匹配零个或多个前面的那一个元素
所谓匹配,是要涵盖 整个 字符串 s的,而不是部分字符串。
说明:
s 可能为空,且只包含从 a-z 的小写字母。
p 可能为空,且只包含从 a-z 的小写字母,以及字符 . 和 *。
示例 1:
输入:
s = "aa"
p = "a"
输出: false
解释: "a" 无法匹配 "aa" 整个字符串。
示例 2:
输入:
s = "aa"
p = "a*"
输出: true
解释: 因为 '*' 代表可以匹配零个或多个前面的那一个元素, 在这里前面的元素就是 'a'。因此,字符串 "aa" 可被视为 'a' 重复了一次。
示例 3:
输入:
s = "ab"
p = ".*"
输出: true
解释: ".*" 表示可匹配零个或多个('*')任意字符('.')。
示例 4:
输入:
s = "aab"
p = "c*a*b"
输出: true
解释: 因为 '*' 表示零个或多个,这里 'c' 为 0 个, 'a' 被重复一次。因此可以匹配字符串 "aab"。
示例 5:
输入:
s = "mississippi"
p = "mis*is*p*."
输出: false
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/regular-expression-matching
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
解法
如果p中没有*这个字符,可以对s和p每一位分别进行比较。已当前比较到索引i为例,s[i] != p[i] && p[i] != '.',则此字符串不匹配。但是当p中包含*这个字符时,*前面的字符可以出现0次或者多次,所以我们可以分别判断出现0次或者出现多次分开讨论。下面以sl表示s的长度,pl表示p的长度。
sl | pl | 是否匹配 |
0 | 0 | true |
>0 | 0 | false |
0 | 1 | false |
>0 | 1 | 先判断s[0]和p[0],然后递归比较s[1-]和p[1-],1-表示从索引1开始到最后的字符 |
0 | >=2 | 判断p[1]是否为*,如果不为*,则false,如果为*,则递归s和p[2-] |
>0 | >=2 | 判断p[1]是否为*,如果不为*,则判断先判断s[0]和p[0],然后递归比较s[1-]和p[1-];如果为*,则判分别递归s,p[2-]以及s[1-]和p[2-],只要这两个任何一个为True ,则s和p匹配 |
从上面的表格看出,在递归时,有很多重复计算,所以增加备忘录,其实是一种从顶向下的动态规划解法,时间复杂度为O(M*N),空间复杂度为O(M*N),M和N分别为字符串长度。
class Solution {
// matchFlag[i]][j]表示i:和j:是否匹配
public boolean matchFlag[][];
public boolean isMark[][];
public boolean isMatch(String s, String p){
matchFlag = new boolean[s.length()+1][p.length()+1];
isMark = new boolean[s.length()+1][p.length()+1];
return dp(s, p, 0, 0);
}
public boolean dp(String s, String p, int i, int j) {
int slength = s.length() - i;
int plength = p.length() - j;
if(isMark[i][j] == true){
return matchFlag[i][j];
}
boolean rs = false;
if(plength == 0){
rs = slength == 0;
}
if (plength == 1){
rs = ((slength > 0) && (s.charAt(i) == p.charAt(j) || p.charAt(j) == '.') && dp(s, p, i+1, j+1));
}
if(plength >= 2){
if(p.charAt(j+1) == '*'){
rs = this.dp(s, p, i, j + 2)
|| ((slength > 0) && ((s.charAt(i) == p.charAt(j) || p.charAt(j) == '.') && dp(s, p, i+1, j)));
}else{
rs = (slength > 0) && (s.charAt(i) == p.charAt(j) || p.charAt(j) == '.') && dp(s, p, i+1, j+1);
}
}
isMark[i][j] = true;
matchFlag[i][j] = rs;
return rs;
}
}
将上述算法改写一下,可以得到从底向上的的动态规划解法,时间复杂度为O(M*N),空间复杂度为O(M*N),M和N分别为字符串长度。
class Solution {
public boolean isMatch(String s, String p){
int slength = s.length();
int plength = p.length();
boolean dp[][] = new boolean[slength+1][plength+1];
dp[slength][plength] = true;
for(int i = slength; i >= 0; i--){
for(int j = plength; j >= 0; j--){
if(j == plength){
dp[i][j] = i == slength;
continue;
}
boolean firstMatch = slength-i > 0 && (s.charAt(i) == p.charAt(j) || p.charAt(j) == '.');
if(plength - j == 1){
dp[i][j] = firstMatch && dp[i+1][j+1];
}else{
if(p.charAt(j + 1) == '*'){
dp[i][j] = dp[i][j+2] || (firstMatch && dp[i+1][j]);
}else{
dp[i][j] = firstMatch && dp[i+1][j+1];
}
}
}
}
return dp[0][0];
}
}
然后dp[i][j]只和dp[i+1][j+1] dp[i][j+2] dp [i+1][j]相关,所以可以再次减少空间复杂度,空间复杂度为O(N), N为p的长度。
class Solution {
public boolean isMatch(String s, String p){
int slength = s.length();
int plength = p.length();
boolean dp[][] = new boolean[2][plength+1];
int cur = 0;
for(int i = slength; i >= 0; i--){
int pre = (cur + 1) % 2;
for(int j = plength; j >= 0; j--){
if(j == plength){
dp[cur][j] = i == slength;
continue;
}
boolean firstMatch = slength-i > 0 && (s.charAt(i) == p.charAt(j) || p.charAt(j) == '.');
if(plength - j == 1){
dp[cur][j] = firstMatch && dp[pre][j+1];
}else{
if(p.charAt(j + 1) == '*'){
dp[cur][j] = dp[cur][j+2] || (firstMatch && dp[pre][j]);
}else{
dp[cur][j] = firstMatch && dp[pre][j+1];
}
}
}
cur = (cur + 1) % 2;
}
return dp[(cur + 1) % 2][0];
}
}