前言
正则表达式是一个非常有强力的工具,其中点号[.]可以匹配任意一个字符,星号[*]可以让之前的那个字符重复任意次数(包括0次)。
一、题目
给我们输入两个字符串s和字符串p, s代表文本,p代表模式串,请你判断模式串p是否可以匹配文本s.我们可以假设模式串只包含小写字母和两种通配符且一定合法。
函数签名如下:
bool isMatch(string s , string p);
二、思路分析
正则表达式算法问题只需要把住一个基本点:看两个字符是否匹配,一切逻辑围绕匹配/不匹配两个种情况张开即可。
如果不考虑*通配符,面对两个待匹配字符s[i] 和 p[j], 我们唯一能做的就是看他们是否匹配:
bool isMatch(string s, string p) {
int i = 0, j = 0;
while (i < s.size() && j < p.size()) {
// 点号[.]可以匹配任意一个字符
if (s[i] == p[j] || p[j] == '.') {
++i;
++j;
} else {
return false;
}
}
return i == j;
}
如果加入[*]通配符,局面就会稍微复杂一些,不过只要分情况来分析,也不难理解。
- 如果匹配,即 s[i] == p[j], 那么两种情况:
一种是 p[j] 有可能会匹配多个字符,比如 s = “aaa”, p = “a *” , 那么p[0] 会通过 * 匹配 3 个字符 “a”。
另一种是p[j] 有可能回匹配0个字符,比如s = “aa” , p = “a * aa”, 由于后面的字符可以匹配s, 所以p[0] 只能匹配 0 次。 - 如果不匹配,即s[i] != p[j] ,只有一种情况:p[j] 只能匹配0次,然后看下一个字符是否能和 s[i] 匹配。比如说 s = “aa”, p = “b*aa”,此时p[0]只能匹配0次。
if (s[i] == p[j] || p[j] == '.') {
// 匹配
if (j < p.size()-1 && p[j + 1] == '*') {
// 有*通配符,可以匹配0次或者多次
} else {
// 无*通配符,老老实实匹配1次
++i;
++j;
}
} else {
// 不匹配
if (j < p.size()-1 && p[j + 1] == '*') {
// 有*通配符,只能匹配0次
} else {
return false;
}
}
三、动态规划解法
由上面可以看出,这就是一个做选择的问题,要把所有可能的选择都穷举一遍才能得出结果。动态规划算法的核心就是状态和选择,状态就是i和j两个指针的位置,选择就是p[j] 选择匹配几个字符。
根据状态,我们可以定义一个dp函数:
bool dp(string &s, int i, string &p, int j)
dp函数的定义:若dp(s,i,p,j) = true, 则表示s[i…] 可以匹配p[j…];若dp(s,i,p,j) = false,则表示s[i…]无法匹配p[j…]。
根据这个定义,我们需要的答案就是 i = 0, j = 0时dp函数的结果,所以可以这样使用这个dp函数:
bool isMatch(string s, string p) {
// 指针 i j 从索引0开始移动
return dp(s,0,p,0);
}
可以根据之前的代码写dp函数的主要逻辑:
bool dp(string& s, int i, string& p, int j) {
if (s[i] == p[j] || p[j] == '.') {
// 匹配
if (j < p.size()-1 && p[j+1] == '*') {
// 通配符匹配 0 次 或 多次
return dp(s,i,p,j+2) || dp(s,i+1,p,j);
} else {
// 常规匹配 1次
return dp(s,i+1,p,j+1);
}
} else {
// 不匹配
if (j < p.size()-1 && p[j+1] == '*') {
// 通配符匹配 0 次
return dp(s,i,p,j+2);
} else {
// 无法继续匹配
return false;
}
}
}
dp函数中base case:一个 base case 是 j == p.size()时, 按照dp函数的定义,这意味着模式串p已经被匹配完了,那么应该看看文本串s匹配到哪里了,如果s也恰好被匹配完,则说明匹配成功:
if (j == p.size()) {
return i == s.size();
}
另一个base case 是 i == s.size() 时,按照dp函数的定义,这种情况就意味着文本串s已经全部被匹配了,那么是不是只要简单地检查一下p是否也匹配完就行了呢?
if (i == s.size()) {
// 这样行吗
return j == p.size();
}
这是不正确的, 此时并不能根据 j 是否完成匹配,只要p[j…] 能够匹配空串,就可以算完成匹配。比如 s = “a”, p = “abc”,当i 走到 s末尾的时候, j 并没有走到p的末尾,但 p 依然可以匹配 s.
int m = s.size(), n = p.size();
// base case
if (i == m) {
// 如果能匹配空串,一定是字符和*成对出现
if ((n - j) % 2 == 1) {
return false;
}
// 检查是否为 x*y*z*这种形式
for (; j + 1 < n; j += 2) {
if (p[j + 1] != '*')
return false;
}
return true;
}
完整代码如下:
bool dp(string& s, int i, string& p, int j) {
int m = s.size(), n = p.size();
// base case
if (j == n) {
return i == m;
}
if (i == m) {
// 如果能匹配空串,一定是字符和*成对出现
if ((n - j) % 2 == 1) {
return false;
}
// 检查是否为 x*y*z*这种形式
for (; j + 1 < n; j += 2) {
if (p[j + 1] != '*')
return false;
}
return true;
}
// 记录状态(i,j), 消除重叠子问题
string key = to_string(i) + "," + to_string(j);
if (memo.count(key))
return memo[key];
bool res = false;
if (s[i] == p[j] || p[j] == '.') {
// 匹配
if (j < p.size()-1 && p[j+1] == '*') {
// 通配符匹配 0 次 或 多次
return dp(s,i,p,j+2) || dp(s,i+1,p,j);
} else {
// 常规匹配 1次
return dp(s,i+1,p,j+1);
}
} else {
// 不匹配
if (j < p.size()-1 && p[j+1] == '*') {
// 通配符匹配 0 次
return dp(s,i,p,j+2);
} else {
// 无法继续匹配
return false;
}
}
// 将当前结果记入备忘录
memo[key] = res;
return res;
}