我的LeetCode代码仓:https://github.com/617076674/LeetCode
原题链接:https://leetcode-cn.com/problems/wildcard-matching/description/
题目描述:
知识点:递归、动态规划
本题和LeetCode010——正则表达式匹配很像,但又有所不同。在LeetCode010——正则表达式匹配中:'.'匹配任意单个字符,'*'匹配零个或多个前面的元素。而在本题中,'?'匹配任意单个字符,'*'可以匹配任意字符串,包括空字符串。虽然匹配条件发生了改变,但解题方法是不变的。
递归终止条件:
(1)当字符串p的长度为0时,若字符串s的长度也为0,返回true。若字符串s的长度不为0,返回false。
(2)当字符串s的长度为0时,如果此时字符串p中的字符全是'*',返回true,否则,返回false。
递归过程:
(1)如果字符串s的最后一个字符能与字符串p的最后一个字符相匹配,我们递归地去判断字符串s去除最后一个字符后的字符串能否与字符串p去除最后一个字符后的字符串相匹配。
(2)如果字符串p的最后一个字符是'*',我们需要去判断字符串s能否与字符串p去除最后一个字符后的字符串相匹配、字符串s去除最后一个字符后的字符串能否与字符串p去除最后一个字符后的字符串相匹配、字符串s去除最后两个字符后的字符串能否与字符串p去除最后一个字符后的字符串相匹配、……、空字符串能否与字符串p去除最后一个字符后的字符串相匹配。只要这其中有一个能匹配,就应该返回true。如果都不能匹配,返回false。
(3)如果不满足前面两种情况,说明字符串s的最后一个字符不能与字符串p的最后一个字符相匹配,直接返回false。
这个思路的时间复杂度比较复杂,最差情况可能是O(ns * np)级别的,其中ns为字符串s的长度,np为字符串p的长度。空间复杂度就是递归深度,最差情况也是O(ns * np)级别的。
JAVA代码:
public class Solution {
//recursion realization
public boolean isMatch(String s, String p) {
int ns = s.length();
int np = p.length();
if(np == 0) {
return ns == 0;
}
if(ns == 0) {
for (int i = 0; i < np; i++) {
if(p.charAt(i) != '*') {
return false;
}
}
return true;
}
if(p.charAt(np - 1) == '?' || s.charAt(ns - 1) == p.charAt(np - 1)) {
return isMatch(s.substring(0, ns - 1), p.substring(0, np - 1));
}
if(p.charAt(np - 1) == '*') {
for (int i = 0; i <= ns; i++) {
if(isMatch(s.substring(0, i), p.substring(0, np - 1))) {
return true;
}
}
}
return false;
}
}
状态定义:f(x, y)------字符串s中[0, x - 1]范围内的字符串能否匹配字符串p中[0, y - 1]范围内的字符串
状态转移:
(1)如果p(y) == '?', f(x, y) = f(x - 1, y - 1)。
(2)如果p(y) == s(x), f(x, y) = f(x - 1, y - 1)。
(3)如果p(y) == '*',f(x, y) = f(x, y - 1) || f(x - 1, y - 1) || f(x - 2, y - 1) || ... || f(0, y - 1)。
时间复杂度是O(ns * np)级别的,其中ns为字符串s的长度,np为字符串p的长度。空间复杂度也是O(ns * np)级别的。
JAVA代码:
public class Solution {
//dynamic programming realization
public boolean isMatch(String s, String p) {
int ns = s.length();
int np = p.length();
boolean[][] matched = new boolean[ns + 1][np + 1];
matched[0][0] = true;
for (int i = 1; i < np + 1; i++) {
matched[0][i] = true;
for (int j = 0; j <= i - 1; j++) {
if(p.charAt(j) != '*') {
matched[0][i] = false;
}
}
}
for (int i = 1; i < matched.length; i++) {
for (int j = 1; j < matched[0].length; j++) {
if(p.charAt(j - 1) == '?' || s.charAt(i - 1) == p.charAt(j - 1)) {
matched[i][j] = matched[i - 1][j - 1];
}else if(p.charAt(j - 1) == '*') {
for (int k = 0; k <= i; k++) {
if(matched[i][j]) {
break;
}
matched[i][j] = matched[i][j] || matched[k][j - 1];
}
}
}
}
return matched[ns][np];
}
}
LeetCode解题报告:
在递归过程中,如果字符串p的最后一个字符是'*',我们只需要去判断字符串s能否与字符串p去除最后一个字符后的字符串相匹配或者字符串s去除最后一个字符后的字符串能否与字符串p相匹配。这里的分类方法变成了不使用该最后一个字符'*'去匹配或者使用该最后一个字符'*'去匹配。具体的原理分析见思路四。
JAVA代码:
public class Solution {
//recursion realization2
public boolean isMatch(String s, String p) {
int ns = s.length();
int np = p.length();
if(np == 0) {
return ns == 0;
}
if(ns == 0) {
for (int i = 0; i < np; i++) {
if(p.charAt(i) != '*') {
return false;
}
}
return true;
}
if(p.charAt(np - 1) == '?' || s.charAt(ns - 1) == p.charAt(np - 1)) {
return isMatch(s.substring(0, ns - 1), p.substring(0, np - 1));
}
if(p.charAt(np - 1) == '*') {
return isMatch(s.substring(0, ns - 1), p.substring(0, np)) || isMatch(s.substring(0, ns), p.substring(0, np - 1));
}
return false;
}
}
状态定义:f(x, y)------字符串s中[0, x - 1]范围内的字符串能否匹配字符串p中[0, y - 1]范围内的字符串
状态转移:
(1)如果p(y) == '?', f(x, y) = f(x - 1, y - 1)。
(2)如果p(y) == s(x), f(x, y) = f(x - 1, y - 1)。
(3)如果p(y) == '*',f(x, y) = f(x, y - 1) || f(x - 1, y)。
为什么可以把思路二中的f(x, y) = f(x, y - 1) || f(x - 1, y - 1) || f(x - 2, y - 1) || ... || f(0, y - 1)简化为f(x, y) = f(x, y - 1) || f(x - 1, y)呢?
以s="acdcb",p="a*c?b"为例进行分析。其动态规划的matched表如下所示。
0("") | 1("a") | 2("a*") | 3("a*c") | 4("a*c?") | 5("a*c?b") | |
0("") | √ | × | × | × | × | × |
1("a") | × | √ | √ | × | × | × |
2("ac") | × | × | √ | √ | × | × |
3("acd") | × | × | √ | × | √ | × |
4("acdc") | × | × | √ | √ | × | × |
5("acdcb") | × | × | √ | × | √ | × |
对于其中第2列,其代表的p串是"a*",是*结尾的。我们计算"a"能否与"a*"匹配时,根据思路二,我们看的是""能否与"a"相匹配以及"a"能否与"a"相匹配。而根据思路四,我们看的是"a"能否与"a"相匹配以及""能否与"a*"相匹配。而我们看""能否与"a*"相匹配时最终看的也是""能否与"a"相匹配,因此两种情况的参照其实是相同的,我们最终看的是""能否与"a"相匹配以及"a"能否与"a"相匹配。对于第2列中的其余行的分析其实也是同理。说明思路二和思路四的最终逻辑其实是一样的,但是思路四比思路二的计算步骤要简洁很多。
从数学式子上分析,
f(x, y) = f(x, y - 1) || f(x - 1, y) = f(x, y - 1) || f(x - 1, y - 1) || f(x - 2, y)
= f(x, y - 1) || f(x - 1, y - 1) || f(x - 2, y - 1) || f(x - 3, y) = ...
= f(x, y - 1) || f(x - 1, y - 1) || f(x - 2, y - 1) || ... || f(1, y - 1) || f(0, y)
= f(x, y - 1) || f(x - 1, y - 1) || f(x - 2, y - 1) || ... || f(1, y - 1) || f(0, y - 1)
这两者也是等价的。
时间复杂度是O(ns * np)级别的,其中ns为字符串s的长度,np为字符串p的长度。空间复杂度也是O(ns * np)级别的。
JAVA代码:
public class Solution {
//dynamic programming realization2
public boolean isMatch(String s, String p) {
int ns = s.length();
int np = p.length();
boolean[][] matched = new boolean[ns + 1][np + 1];
matched[0][0] = true;
for (int i = 1; i < np + 1; i++) {
matched[0][i] = true;
for (int j = 0; j <= i - 1; j++) {
if(p.charAt(j) != '*') {
matched[0][i] = false;
}
}
}
for (int i = 1; i < matched.length; i++) {
for (int j = 1; j < matched[0].length; j++) {
if(p.charAt(j - 1) == '?' || s.charAt(i - 1) == p.charAt(j - 1)) {
matched[i][j] = matched[i - 1][j - 1];
}else if(p.charAt(j - 1) == '*') {
matched[i][j] = matched[i - 1][j] || matched[i][j - 1];
}
}
}
return matched[ns][np];
}
}
LeetCode解题报告: