题目描述
解题思路
通过提供的例子,感觉只要分情况考虑就OK,但是开始动手写代码的时候,果然,被标位困难还是有道理的,算了,还是老老实实从头再来吧,开始从例子分析题目:
经过归纳总结我们可以发现,匹配模式中无非就三种情况:
'.'
:该模式可以匹配单个任意字符,只要匹配任意单个字符就可以跳到下一步'*'
:该模式必须和其他两种情况搭配使用,表示0个或多个前面的那个字符- 其他字符:主要判断该字符与待匹配字符串中的字符是否相等即可,相等就可以跳到下一步,不相等直接返回false
对于
'*'
这种情况比较麻烦,由于它必须和其他两种情况搭配使用,因此我们在匹配到其他两种情况时,还要再往下面看一个字符,看看是不是'*'
,如果是,就需要继续进行处理了
每次从p中拿出一个字符来与s中的字符进行匹配
如果该字符后面的字符不是*,那么直接与s中对应的字符进行匹配即可,如果匹配成功,那么就将两个游标往后移动一位,如果匹配过程中遇到不相等的时候,直接返回false
如果该字符后面的字符是*,那么就要分情况
- 一种是匹配0个,那么只需要跳过p中的这两个字符,继续与s中的字符进行比较即可
- 另外一种是匹配多个,那么将s中的游标往后移动一个,进行判断,这两个条件只要其中一个能满足即可
1、递归法
从上述中,我们可以看出,对于每个字符的匹配情况,其实是类似的,很自然的想到递归的方式,那么我们就要考虑递归的停止条件以及递归的过程:
递归的停止条件:
- 如果s字符串的长度为0,此时字符串p当且仅当有形如
"a*b*c*d*"
这种格式时,返回true,否则返回false - 如何s字符串的长度不为0,而p字符串的长度为0,返回false
递归的过程:
- 如果s的第一个字符与p的第一个字符相等,或者说p的第一个字符为
.
且后一个字符不为*
时,那么我们直接看字符串s中除去第一个字符后的字符串能否与字符串p中除去第一个字符的字符串匹配 - 如果p字符串中的第一个字符后面的一个字符是
*
,那么此时就要分情况了:
2.1 一种是匹配0个,那么只需要跳过p中的这两个字符,继续与s中的字符进行比较即可
2.2 另外一种是匹配多个,那么将s中的游标往后移动一个,继续判断
2.3 这两个条件只要其中一个能满足即可
Java代码:
class Solution {
public boolean isMatch(String s, String p) {
//代码完整性
if (p.length() <= 0) return s.length() <= 0;
//第一个字符匹配
boolean match = (s.length() > 0 && (s.charAt(0) == p.charAt(0) || p.charAt(0) == '.'));
//特殊情况
if (p.length() > 1 && p.charAt(1) == '*'){
//匹配0个,跳过p中的这两个字符;或多个,s往后移动一个继续匹配
return isMatch(s, p.substring(2)) || (match && isMatch(s.substring(1), p));
} else {
//一般情况
return match && isMatch(s.substring(1), p.substring(1));
}
}
}
很显然,效果不咋地,下面我们来看看动态规划的解法
2、动态规划法
什么是动态规划?
- 动态规划与分治方法类似,都是通过组合子问题的解来来求解原问题的。
- 分治方法将问题划分为互不相交的子问题,递归的求解子问题,再将它们的解组合起来,求出原问题的解
- 动态规划与之相反,动态规划应用与子问题重叠的情况,即不同的子问题具有公共的子子问题(子问题的求解是递归进行的,将其划分为更小的子子问题)
- 在这种情况下,分治方法会做许多不必要的工作,他会反复求解那些公共子子问题。而动态规划对于每一个子子问题只求解一次,将其解保存在一个表格里面,从而无需每次求解一个子子问题时都重新计算,避免了不必要的计算工作。
我们举个简单的例子,就是求解斐波那契数列的问题:
我们使用分治思想来解答fib(6)
,我们知道,fib(6)=fib(5)+fib(4)
,那么原问题就被化解为求解fib(5)
和fib(4)
,然后对fib(5)
和fib(4)
在进行化解可以得到如下图示:
从上图我们可以发现,里面出现了很多重复的计算,这将会浪费非常多的时间
相反,如果使用动态规划的方法来求解,就会减少很多重复计算
要计算fib(6)
,因此我们先计算fib(3)=fib(2)+fib(1)
,再计算fib(4)=fib(3)+fib(2)
和fib(5)=fib(4)+fib(3)
,这样就能计算得到fib(6)=fib(5)+fib(4)
的结果了,在动态规划中有几个比较关键的概念:
子问题、状态、状态空间、初始状态、状态转移方程
- 子问题:与原问题形式相同或者类似,只不过规模变小了,子问题都解决后,原问题即解决。
- 状态:与子问题相关的各个变量的一组取值即为状态,状态与子问题是一对一或一对多的关系,代表着子问题的解。上面的例子,状态就是
fib(n)
的值。 - 状态空间:由所有状态构成的集合,上面的例子比较简单,状态空间是一维空间。
- 状态初始条件:即状态的初始状态,上面的栗子里
fib(1) = 1
和fib(2) = 1
就是初始条件。 - 状态转移方程:用来表示状态之间是如何转换的方程,即如何从一个或者多个已知的状态求出另一个状态,可以使用递推公式表示。上面例子的公式为
fib(n) = f(n - 1) + f(n -2) (n > 2)
关于本题的动态规划解法的思路如下:
为了方便起见,我们引用如下符号:
s[i:]
:表示字符串s中从第i个字符到最后一个字符组成的子串,p[j:]
也类似match(i,j)
:表示s[i:]
和p[j:]
的匹配情况,如果能匹配,则置为true
,否则置为false
那么对于match(i,j)
的值,取决于 p[j+1]
是否为 *
curMatch = i < s.length() && s[i] == p[j] || p[j] == '.';
p[j+1] != *,match(i,j) = curMatch && match(i+1,j+1)
p[j+1] = *,match(i,j) = match(i,j+2) || curMatch && match(i+1,j)
Java代码:
enum Status{
TRUE,FALSE
}
class Solution {
Status[][] sta;
public boolean isMatch(String s, String p) {
sta = new Status[s.length() + 1][p.length() + 1];
return match(0, 0, s, p);
}
public boolean match(int i, int j, String s, String p){
//如果状态不为空,则判断状态值是否为TRUE,并返回
if(sta[i][j] != null){
return sta[i][j] == Status.TRUE;
}
boolean ans;
//如果模式匹配完了
if(j == p.length()){
ans = (i == s.length()); //查看还有没有字符
}else{
boolean curMatch = (i < s.length() && (p.charAt(j) == s.charAt(i) || p.charAt(j) == '.'));
if(j+1 < p.length() && p.charAt(j+1) == '*'){
ans = (match(i, j+2, s, p) || curMatch && match(i+1, j, s, p));
}else{
ans = curMatch && match(i+1, j+1, s, p);
}
}
sta[i][j] = ans ? Status.TRUE : Status.FALSE;
return ans;
}
}
Python3代码:
- 1、建立一个二维表status,
status[i][j]
表示s的子串[0,i)和p的子串[0,j)匹配,值为True,否则为False - 2、当p为空串时,只有s为空,才能匹配,所以第一列中只有
status[0][0]=True
- 3、当s为空,只有p为空或者为形如"a*b*c*"才能匹配
- 4、每次s字符串往下走一个字符,和所有的p子串进行匹配,接下来分两种情况进行分类。
- 假设当前位置为
status[i][j]
- 若
p[j-1] == '*'
时,'*'
的用法分为两种(1:匹配0个 2:匹配1个或多个),要想status[i][j] =1
,需要满足下列条件中的任一个: -
status[i][j-2] = 1
时,此时"*"代表空串 -
status[i-1][j] = 1
时且满足(p[j-2] == s[i-1] or p[j-2] == "."
),此时"*"代表对前一字符的复制 - 若
p[j-1]!= "*"
时,要想status[i][j] = 1
,需满足: -
p[j-1] == s[i-1] or p[j-1] == "."
,且还要判断前面的是否匹配,即status[i-1][j-1]
的值是否为True - 5、最终返回
status[len(s)][len(p)]
class Solution:
def isMatch(self, s, p):
# 状态空间
status = [[False for j in range(len(p)+1)] for i in range(len(s)+1)]
# s和p为空时
status[0][0] = True
# 处理第一行
for j in range(1,len(p)+1):
# 如果遇到*,并且下标大于等于2,跳过p的这两个字符
if p[j-1] == '*' and j >= 2:
status[0][j] = status[0][j-2]
# 遍历s的每个字符
for i in range(1,len(s)+1):
# 遍历p的每个字符
for j in range(1,len(p)+1):
# 如果j-1位置为*
if p[j-1] == '*':
# 匹配前面字符0个则跳过p这两个字符j+2,匹配前面多个,
status[i][j] = status[i][j-2] or ( status[i-1][j] and (p[j-2] == s[i-1] or p[j-2] == '.') )
else: # 否则,就比较前面位置是否为.或者与s对应的相等,并且判断status[i-1][j-1]
status[i][j] = (p[j-1] =='.' or p[j-1] == s[i-1]) and status[i-1][j-1]
return status[len(s)][len(p)]