10. 正则表达式匹配
问题描述
给你一个字符串 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 = “cab”
输出: true
解释: 因为 ‘*’ 表示零个或多个,这里 ‘c’ 为 0 个, ‘a’ 被重复一次。因此可以匹配字符串 “aab”。
示例 5:
输入:
s = “mississippi”
p = “misisp*.”
输出: false
解决方法:
方法1:回溯法(暴力法)
如果没有星号(正则表达式中的 * ),问题会很简单——我们只需要从左到右检查匹配串 s 是否能匹配模式串 p 的每一个字符。
当模式串中有星号时,我们需要检查匹配串 s 中的不同后缀,以判断它们是否能匹配模式串剩余的部分。一个直观的解法就是用回溯的方法来体现这种关系。
在这种情况下,当回溯多个路径失败时,可能重复计算同一个路径的结果。因此可用动态规划对回溯法进行优化。
下述代码需要注意边界条件,当匹配串s检索到最后一个字母时,模式串可能不为空。
另外,下述代码也存在一定冗余,即判断匹配串中的一个字母是否与模式串中的一个字母匹配。可通过迭代调用简化代码。
//代码1:循环
class Solution1 {
public boolean isMatch(String s, String p) {
return match(s,p,0,0);
}
private boolean match(String s, String p, int l1, int l2){
int i=l1,j=l2;
while(i<=s.length() && j<p.length()){
if(j==p.length()-1 || p.charAt(j+1)!='*'){
if(i==s.length()) return false;
if(p.charAt(j)!='.' && s.charAt(i) != p.charAt(j)){
return false;
}
i++;
j++;
}else{
for(int k=i;k<=s.length();k++){
if(p.charAt(j)=='.' || k==i ||s.charAt(k-1) == p.charAt(j)){
if(match(s,p,k,j+2)){
return true;
}
}else{
break;
}
}
return false;
}
}
if(i==s.length()&&j==p.length()){
return true;
}else{
return false;
}
}
//代码2:迭代法
class Solution2 {
public boolean isMatch(String s, String p) {
return match(s,p,0,0);
}
private boolean match(String s, String p, int l1, int l2){
if(l2 == p.length()) return l1==s.length();
boolean firstMatch=(l1 < s.length() && (p.charAt(l2) == '.' ||s.charAt(l1) == p.charAt(l2)));
if(l2<p.length()-1 && p.charAt(l2+1) == '*'){
return (match(s,p,l1,l2+2) || (firstMatch && match(s,p,l1+1,l2)));
}else{
return (firstMatch && match(s,p,l1+1,l2+1));
}
}
}
解决方法2:动态规划(优化回溯法)
动态规划:使用memo保存计算过程中的中间结果。基于暴力法的基础上,我们可以实现自顶向下以及自底向上的方法。
其中,自顶向下的方法只需要判断数组是否已经计算过。如果计算过则直接读取数组。较为方便,且在该题目中,自顶向下方法优于自底向上方法。
自底向上的方法。不需要迭代函数,只需要通过循环依次计算相应的值。在实际机试时,应先推出暴力法的计算过程(通常暴力法无法通过所有测试集)。再基于暴力法推导动态规划的计算式。
class Solution3 {
Boolean [][]memo;
public boolean isMatch(String s, String p) {
memo=new Boolean[s.length()+1][p.length()+1];
return match(s,p,0,0);
}
private boolean match(String s, String p, int l1, int l2){
if(memo[l1][l2]!=null) return memo[l1][l2];
boolean res;
if(l2 == p.length()) res=l1==s.length();
else{
boolean firstMatch=(l1 < s.length() && (p.charAt(l2) == '.' ||s.charAt(l1) == p.charAt(l2)));
if(l2<p.length()-1 && p.charAt(l2+1) == '*'){
res = (match(s,p,l1,l2+2) || (firstMatch && match(s,p,l1+1,l2)));
}else{
res = (firstMatch && match(s,p,l1+1,l2+1));
}
}
memo[l1][l2]=res;
return res;
}
}
public class Solution4 {
public boolean isMatch(String s, String p) {
boolean [][] memo = new boolean[s.length()+1][p.length()+1];
memo[s.length()][p.length()]=true;
for(int i=s.length();i>=0;i--){
for(int j=p.length()-1;j>=0;j--){
boolean first_match=(i<s.length()&&(p.charAt(j)=='.'||s.charAt(i)==p.charAt(j)));
if(j+1<p.length()&&p.charAt(j+1)=='*'){
memo[i][j]= memo[i][j+2] || first_match && memo[i+1][j];
}else {
memo[i][j] = first_match && memo[i + 1][j + 1];
}
}
}
return memo[0][0];
}
}