题目描述
给定一个字符串 s 和一个字符规律 p,实现一个支持 ‘.’ 和 ‘’ 的正则表达式匹配。
‘.’ 匹配任意单个字符
'’ 匹配零个或多个前面的那一个元素
所谓匹配,是要涵盖 整个 字符串 s的,而不是部分字符串。
说明:
s 可能为空,且只包含从 a-z 的小写字母。
p 可能为空,且只包含从 a-z 的小写字母,以及字符 . 和 *。
算法设计
问题分析
输入输出:
①输入:
s = “aa”
p = “a”
输出: false
解释: “a” 无法匹配 “aa” 整个字符串。
②输入:
s = “aa”
p = “a*”
输出: true
解释: 因为 ‘*’ 代表可以匹配零个或多个前面的那一个元素, 在这里前面的元素就是 ‘a’。因此,字符串 “aa” 可被视为 ‘a’ 重复了一次。
③输入:
s = “ab”
p = “."
输出: true
解释: ".” 表示可匹配零个或多个(’*’)任意字符(’.’)。
④输入:
s = “aab”
p = “cab”
输出: true
解释: 因为 ‘*’ 表示零个或多个,这里 ‘c’ 为 0 个, ‘a’ 被重复一次。因此可以匹配字符串 “aab”。
算法设计方法比较选择
此问题的关剑是解决对‘*’符号的处理,因此我选择了2种算法来进行对比
回溯法
当遇到‘*’符号时,需要判断对前面的字符重复多少次,因此自然的想到回溯法来对问题的解空间树进行搜索,每重复一次解空间树便出现一个节点,同时增加适当的剪枝函数和限界函数来减少时间和空间上的开销。
动态规划法
仔细分析可以发现此问题的最优解包含其子问题的最优解,也就是说如果p和s能匹配即它们的子序列在某种程度上能进行匹配。一个自然的想法是将中间结果保存起来,因此考虑使用动态规划法进行求解。
算法设计方法
回溯法
设计思想
通过递归的思想来实现:当没有‘’符号时,题目是一个字符串比较问题,只需要对每个字符进行比较即可。当加上‘’符号之后,就要分情况讨论,假设‘*’之前有符号存在那么这个字符可以重复0-n次,依次判断字符重复0-n次的情况,通过递归调用函数来实现。但是这样实现的解空间树过于巨大,因此在实现过程中加入一些剪枝条件,减小开销。
方法
1、如果p为空,s为空匹配,s非空不匹配;
2、s非空,p == s || p == '.'时第一个字符匹配;
3、(p+1) != ‘’,则递归判断剩下的是否匹配 IsFirstMatch&& isMatch(++s, ++p)
4、(p+1) == ‘*’,则有两种情况匹配:
a: *匹配0个字符,s匹配剩下的,即isMatch(s, p+2)
b: *匹配1个字符,继续用p匹配剩下的s,即IsFirstMatch && isMatch(s+1, p)
框图
拿s=” ab ” , p=” .* ” 来举例:
代码
bool isMatch(char* s, char* p) {//s为字符串,p为正则表达式
if (!*p) return !*s;
bool IsFirstMatch = s[0]&&(s[0] == p[0] || p[0] == '.');//判断第一个字符串是否匹配
if (strlen(p) >= 2 && p[1] == '*') {//如果正则表达式的第二个字母为*
return isMatch(s, p + 2)||(IsFirstMatch && isMatch(s+1, p));
}
else//如果第二个字母不为*
{
return IsFirstMatch && isMatch(s + 1, p + 1);
}
}
动态规划
设计思路
因为题目拥有 最优子结构 ,自然想到将中间结果保存起来。使用“备忘录”递归的方法来降低复杂度,就是使用两个变量 i, j 记录当前匹配到的位置,从而避免使用子字符串切片,并且将 i, j 存入备忘录,避免重复计算即可。
方法
- 先创建“备忘录”d[i][j],i为s的前i项,j为p的前i项。
- 如果s[i]==p[j]或者p[j]为通配符,那么可以推出s[i+1][p+1]=s[i]p[j],因为s的第i+1项等于p的第j项时,s的前i项如果等于p的前j项,那么s的前i+1项等于p的前j+1项;s的前i项如果不等于p的前j项,那么s的前i+1项也不等于p的前j+1项。
- 当p的第j项也就是p[j]为‘’时,由于‘’与前面的字符有关,比较p[j]前面的字符p[j-1]和s[i]的关系,前面的字符可以重复0次也可以重复1-n次,因此分下面2种情况:
a.如果‘’前面的一个字符与s[i]匹配不上,则可说明‘’匹配前面字符0次, 忽略‘’以及前面的字符,看p[j-2]与s[i]是否匹配。此时的dp[i][j]=dp[i][j-2]。
b.如果‘’前面的一个字符与s[i]匹配上了,则可说明‘*’匹配前面字符0-n次,当匹配0次时,方法同a;当匹配多次时需要将s[i]前面的字符重新与p进行比较,也就是d[i][j]=dp[i-1][j]。 - 除了以上三种情况,dp[i][j]=false。
状态转移方程
由前面的分析可得出状态转移方程
代码
空间复杂度O(n^2)
bool isMatch(char* s, char* p) {
int slength = strlen(s), plength = strlen(p);
if (plength == 0) {
return slength == 0;
}
if (slength == 0 && plength == 1) {
return false;
}
//初始化
bool dp[slength + 1][plength + 1]; //创建矩阵
dp[0][0] = true;
dp[0][1] = false;
for (int i = 2; i < plength + 1; i++) {//初始化第0列
dp[0][i] = (p[i - 1] == '*' && dp[0][i - 2]);
}
for (int i = 1; i < slength + 1; i++) {//初始化第0行
dp[i][0] = false;
}
//填表
for (int i = 1; i < slength + 1; i++) {
for (int j = 1; j < plength + 1; j++) {
if (j >= 2 && p[j - 1] == '*') { //当遇到‘*’时
dp[i][j] = (dp[i - 1][j] && (p[j - 2] == s[i - 1] || p[j - 2] == '.')) || dp[i][j - 2];
}
else if (p[j - 1] == '.' || p[j - 1] == s[i - 1]) {//当不为‘*’时
dp[i][j] = dp[i - 1][j - 1];
}
else {
dp[i][j] = false;
}
}
}
return dp[slength][plength];
}
代码优化
时间复杂度显然不能继续优化了,但是动态规划法中,空间复杂度,也就是‘备忘录’通常可以优化,之前因为有个二维数组,空间复杂度为O(n^2)。观察状态转移方程:
可知dp[i][j]的值仅与前面2列有关,因此可以只创建一个i行3列的数组dp[i][3]来作为备忘录,此时空间复杂度O(n)。
bool isMatch(char* s, char* p) {
int slength = strlen(s);
int plength = strlen(p);
if (plength == 0) {
return slength == 0;
}
if (slength == 0 && plength == 1) {
return false;
}
//初始化
bool dp[slength + 1][3]; //创建矩阵
dp[0][0] = true;
dp[0][1] = false;
dp[0][2] = (p[1] == '*' && dp[0][0]);//初始化第0列
for (int i = 1; i < slength + 1; i++) {//初始化第0行
dp[i][0] = false;
}
int y=0;
//填表
for (int j = 1; j < plength + 1; j++) {
if(j%3==0){
y++;
}
if(y>=1){
dp[0][j%3]=(p[(3*y)+(j%3)-1] == '*' && dp[0][(j+1)%3]);
}
for (int i = 1; i < slength + 1; i++) {
if (j >= 2 && p[j - 1] == '*') { //当遇到‘*’时
dp[i][j%3] = (dp[i - 1][j%3] && (p[j - 2] == s[i - 1] || p[j - 2] == '.')) || dp[i][(j - 2)%3];
}
else if (p[j - 1] == '.' || p[j - 1] == s[i - 1]) {//当不为‘*’时
dp[i][j%3] = dp[i - 1][(j - 1)%3];
}
else {
dp[i][j%3] = false;
}
}
}
return dp[slength][plength%3];
}
参考
[1] 动态规划 - 从 0 讲解
[2] 回溯和动态规划