之前突然发了2篇关于算法的博客,其实就是为了学习动态规划,并解决他,所以这篇加个后缀(3)。不是很了解的朋友可以去看看
题目是这样的:
给你一个字符串 s 和一个字符规律 p,请你来实现一个支持 ‘.’ 和 ‘*’ 的正则表达式匹配。
所谓匹配,是要涵盖 整个 字符串 s的,而不是部分字符串。
示例 1:
输入:s = “aa”, p = "a"
输出:false
解释:“a” 无法匹配 “aa” 整个字符串。
示例 2:
输入:s = “aa”, p = "a*"
输出:true
解释:因为 ‘*’ 代表可以匹配零个或多个前面的那一个元素, 在这里前面的元素就是 ‘a’。因此,字符串 “aa” 可被视为 ‘a’ 重复了一次。
示例 3:
输入:s = “ab”, p = “.“
输出:true
解释:”.” 表示可匹配零个或多个(’*’)任意字符(’.’)。
提示:
题目来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/regular-expression-matching
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
起初看完题目,我用积累的经验去解决,到后面各种if、else写到你麻痹。跟我一样的,可以先去学习下之前2篇文章。然后开始吧、
//我们要判断s,是否符合p的正则表达式
public boolean isMatch(String s, String p) {
}
递归是把所有情况都循环了一遍。我么简单理下
所以递归结束的条件出来了:
//我们要判断s,是否符合p的正则表达式
public boolean isMatch(String s, String p) {
//找到跳出循环的条件
if (p.isEmpty()) {
return s.isEmpty();
}
}
这里可能和之前不太一样,但逻辑思维是一样的。之前说的是算出青蛙一共有几种跳台阶的方式。这是算出 s,p 是否匹配。
我们来理一下匹配规则的逻辑:
看完以上那么可以得出
s,p是否匹配 可以拆解为: (p的第二个元素是 * 且和s匹配) 和(p的第二个元素不是 * 且和s匹配)
那么就开始解决问题吧,这里一步一步演变。首先我们的的分界在第二个字符是否是 * ,既然是第二个字符,所以字符长度必须大于等于2
if (p.length()>=2&&p.charAt(1)=='*'){
}else {
}
我们先分析else里的因为简单。如果不满足条件,那么我们只要匹配首字母,如果匹配,移除首字母继续匹配,否则返回false。因为前面跳出环境已经判断了 p 是否为空,在首字母匹配的时候,如果 s 为空的话必然是false
if (p.length() >= 2 && p.charAt(1) == '*') {
} else {
//s不为空
//s 首字母和 p 首字母匹配
//或者p 首字母为 ‘.’,匹配任何字符
//匹配上了,我们把第一个字符移除,继续匹配后面的
boolean first_match = !s.isEmpty() && (s.charAt(0) == p.charAt(0) || p.charAt(0) == '.');
return first_match && isMatch(s.substring(1),p.substring(1));
}
再来看看上面的条件,第2个元素是 * ;这里分2种情况,上面讨论过了,只要2种情况有一种为ture,就是true。
if (p.length() >= 2 && p.charAt(1) == '*') {
boolean first_match = !s.isEmpty() && (s.charAt(0) == p.charAt(0) || p.charAt(0) == '.');
return isMatch(s, p.substring(2)) || (first_match && isMatch(s.substring(1), p));
} else {
}
这样所有的情况都讨论完了。我们把first_match提出来。最后递归解决的方法:
//我们要判断s,是否符合p的正则表达式
public boolean isMatchDig(String s, String p) {
//找到跳出循环的条件
if (p.isEmpty()) {
return s.isEmpty();
}
//首字母是否匹配
boolean first_match = !s.isEmpty() && (s.charAt(0) == p.charAt(0) || p.charAt(0) == '.');
if (p.length() >= 2 && p.charAt(1) == '*') {
return isMatch(s, p.substring(2)) || (first_match && isMatch(s.substring(1), p));
} else {
return first_match && isMatch(s.substring(1), p.substring(1));
}
}
这里需要先看下之前dijkstra算法。不然完全看不懂的。动态规划算法会有一个容器,把算过的结果放在这个容器里。后续需要重复计算的直接在这个容器里取,从而时间复杂度和空间复杂度都得到了很大的提升。也就是说,这里我们会用到二维数组。
我们先来看张图:
代表什么意思呢,这里解释下。这里是用2个字符解释,当然实际2个字符都是随机的。
横轴代表字符s: abbc (注意我们在字符前加了个空字符“ ”)
纵轴代表字符p: ab*c
第一行的第一列的T,代表用p的第一个空字符“ ” 去匹配s的第一个空字符“ ”。这个结果我们是知道的。一定为true
那么二维数组dp[i][j] 代表的意思就是:s的前i个字符 和 p的前j个字符是否匹配。在这里我们的dp[0][0] = true.
所以我们要把这个格子填满,最后得出 ? 里的是true还是false
我在代码里注释吧,感觉会更清晰按步骤来。
public boolean isMatch(String s, String p) {
//为了配合表格里讲解,我首先把原字符也加上一个空字符,便于理解(只是为了便于理解,加上字符会增加空间和时间复杂度)
s = " " + s;
p = " " + p;
int m = s.length();
int n = p.length();
//申明一个二维数组,并初始化dp[0][0]。这里我们要知道初始化的值期初都是false
boolean[][] dp = new boolean[m][n];
dp[0][0] = true;
//我们先把2个空字符的情况考虑下,想想有哪些在空字符的时候会为true的情况
// 1、如果p为空字符,s必为空字符才会为true。
// 2、如果s为空字符,p可以为空,也可以有另外一种情况,就是 a* ,它代表0个或多个,所以也可以为空
//下面是给以上2个条件赋值为true的代码。
//这里我稍微解释下:因为我前面加个空字符,且按照题目描述,
//这里的出现 * 号时,i一定是在第3个元素以及后面出现,所以i>=2,不会越界
//举例如s为空“ ”,p为“ a*”,把a*移除也是“ ”。配合下面代码看其实这里的i=2,带入dp[0][2] = dp[0][0],因为dp[0][0]=true
//知道dp[0][2] = true了。那循环到 p为“ a*b*”带入下面代码是不是 dp[0][4] = dp[0][2]
// 这就是动态规划,把计算过的存入二维数组,用的时候取出来。
for (int i = 0; i < n; i++) {
if (p.charAt(i) == '*') {
dp[0][i] = dp[0][i - 2];
}
}
... 后续代码
}
通过递归我们知道,关键在于遇到这个 * 号 。步骤2是接着步骤1的代码的,分开讲解。我们对二维数组遍历。记住dp[i][j] 代表的是s的前i个字符和p的前j个字符相匹配。
//因为步骤1已经考虑了空字符的情况,我们让 i=1,j=1跳过空字符的情况
for (int i = 1; i < m; i++) {
for (int j = 1; j < n; j++) {
//当p字符为*号时,这里有多种情况,我们先看下面的。
if (p.charAt(j) == '*') {
//不为 * 号时无非就是‘.’或字母。如果是 . 匹配任意字母。不是点的话那么 2个字符要匹配
} else if (p.charAt(j) == '.' || p.charAt(j) == s.charAt(i)) {
//如果满足以上条件。dp[i][j]的匹配情况就是他们前面的匹配情况
//举例 s为abc,p也为abc。如果首字母匹配了,abc和abc的匹配情况就是 bc和bc的匹配情况
dp[i][j] = dp[i - 1][j - 1];
}
}
}
看上面的情况,如果p.charAt(j) == ‘*’ 的话,我们要看他前面的字符,分2种情况,为‘ .’ 或为 字母
for (int i = 1; i < m; i++) {
for (int j = 1; j < n; j++) {
//当前字符为 * 号。分2种情况
if (p.charAt(j) == '*') {
if (p.charAt(j - 1) == '.') {
// 前一个字符是‘.’,又分3种情况:
//1、匹配 0 次,那把p移除前2个字符和s的前i个字符匹配为:dp[i][j-2]
//2、匹配 1 次,那么把p的 * 号移除,用 p 的前j-1个字符和s的前一个字符匹配:dp[i][j-1]
//3、匹配 2 次及以上,说明dp[i][j]能匹配,dp[i-1][j]也能匹配,才至少匹配2次
// 如果满足以上情况的任意一种 dp[i][j] = true;
if (dp[i][j - 2] || dp[i][j - 1]|| dp[i - 1][j]) {
dp[i][j] = true;
}
} else {
//前一个字符是字母,这里和上面基本一样,不同的是 匹配多次
//匹配多次也是dp[i-1][j]并且,s的当前字符要和p的 * 好的前的字符要匹配
//举例,s为aa,p为a*。那么既要满足dp[i - 1][j]也就是s前的a和a*匹配也要满足s后的a和p的a匹配。
if (dp[i][j - 2] || dp[i][j - 1]
|| (s.charAt(i) == p.charAt(j - 1) && dp[i - 1][j])) {
dp[i][j] = true;
}
}
}
}
}
最后得出代码为:
public boolean isMatch(String s, String p) {
s = " " + s;
p = " " + p;
int m = s.length();
int n = p.length();
boolean[][] dp = new boolean[m][n];
dp[0][0] = true;
for (int i = 0; i < n; i++) {
if (p.charAt(i) == '*') {
dp[0][i] = dp[0][i - 2];
}
}
for (int i = 1; i < m; i++) {
for (int j = 1; j < n; j++) {
if (p.charAt(j) == '*') {
if (p.charAt(j - 1) == '.') {
if (dp[i][j - 2] || dp[i][j - 1]
|| dp[i - 1][j]) {
dp[i][j] = true;
}
} else {
if (dp[i][j - 2] || dp[i][j - 1]
|| (s.charAt(i) == p.charAt(j - 1) && dp[i - 1][j])) {
dp[i][j] = true;
}
}
} else if (p.charAt(j) == '.' || p.charAt(j) == s.charAt(i)) {
dp[i][j] = dp[i - 1][j - 1];
}
}
}
return dp[m - 1][n - 1];
}
终于把动态算法整明白了。。。