LeetCode-Regular Expression Match 解析与备忘
问题描述
题目链接:https://leetcode.com/problems/regular-expression-matching/description/
给定义一个字符串s
与匹配模式p
,返回一个bool
值:
- 若s
与p
完全匹配,返回True
;
- 反之,返回False
。其中,
s
中只包含a-z
,而p中包含a-z
,以及.
和*
。.
可以代表任一字符,*
表示前面的字符出现零次、一次或多次。Example
s = "ab"
p = ".*"
返回True
,因为p
可以看作为..
,匹配ab
。Example
s = "mississippi"
p = "mis*is*p*."
返回False
。
- 网上此类博客甚多,此处主要作为自己的备忘,分析肯定不及大佬们精细,偶有错误,无伤大雅。
递归
简单直接的想法,就是依次对比每个字符:
1. 如果p
在第i
个位置的字符p[i]!=*
,判断p[i]==s[i]
,或者p[i] == '.'
,相等就跳到i+1
的位置,继续对比p[i+1]
与s[i+1]
,否则返回False
,不匹配;
2. 如果p[i]=='*'
,检查前一个位置p[i-1]
与s[i]
是否相等,
2a. 若p[i-1]!=s[i]
,表明*
只能表示0个元素,继续匹配p[i+1]
与s[i]
;
2b. 若p[i-1]==s[i]
,则*
可能代表至少一个元素,也有可能代表0个元素,任何一种情况为真时,都可以返回True
。(由于多个元素可以分解为单个元素,所以只需要递归检查*
代表一个元素和代表0个元素两种情况)
实现如下:
bool isMatch(string s, string p)
{
/* p.size() == 0 */
if(p.empty()) return s.empty();
/* p.size() == 1 */
if(p.size() == 1) return s.size() == 1 && (s[0] == p[0] || p[0] == '.');
/* p.size() >= 2 */
if (p[1] != '*')
return !s.empty() && (s[0] == p[0] || p[0] == '.') && isMatch(s.substr(1), p.substr(1));
else
{
if (s.empty() || (s[0] != p[0] && p[0] != '.'))
return isMatch(s, p.substr(2));
else
return isMatch(s.substr(1), p) || isMatch(s, p.substr(2));
}
}
主要问题就是理清逻辑,算无遗策。另有博客从字符串末尾开始匹配,其判断过程大同小异,不赘述。
动态规划
我不甚了解动态规划的思想,有大佬推荐入门博客,但是我还没看–.
简要说下这里用动态规划的问题,我的初步理解是减少一些不必要的计算开销。很多时候有些过程都是重复计算的,但是因为在递归里,不停地跳入跳出,没有保存中间值,所以没办法利用好这些已经计算的值,而进行许多的重复计算。
我们考虑一张bool
类型的表,其值表示匹配与否,一个带图的例子可以参照这里。
以s = "bbbba"
、p = ".*a*a"
为例,我们首先构建一张表T
,1表示匹配,0表示不匹配。通常,我们都会多一行一列(此处为空字符串)用于初始化的方便:
其中,为了保持s
、p
和T
的下标看起来具有一致性,我们将初始的行数和列数记为-1。但第-1行和-1列在初始化时已经赋予,实际中i
和j
从0开始取。T[-1][-1]
为1,因为空字符串与空字符串相等,其余为零;
基于之前递归的规则,我们考虑我们的状态转移:
- 当p[i]!='*'
时,当且仅当p[i]==s[i] || p[i]=='.'
时,T[i][j]
有机会为1。当之前的子字符串都匹配时,此时当前字符也匹配,才会有T[i][j]=1
。注意到我们在初始状态多了一行一列,但是在建立表T
时,默认初始行(列)为-1,因此T[i][j]
实际存储的是p[i]
与s[j]
的状态信息。在此之前的子字符串匹配状态存储在T[i-1][j-1]
,由此T[i][j]=(p[i]==s[i] || p[i]=='.') && T[i-1][j-1]
(类比在递归的此情况下,我们会将s
的下一位和p
的下一位进行匹配:s[j+1]
与p[i+1]
),其余情况为0;
- 当p[i]=='*'
时,和之前的递归一样,我们考虑两种情况:*
代表0次,以及*
代表1次(多次也可以由1次叠加)。当p[i-1]!=s[j] && p[i-1]!='.'
,此时*
只能代表0次,而此时的状态应该由此前p
在i-2
时刻决定(因为i-1
时刻与i
时刻将要被踢除),即T[i][j]=T[i-2][j]
(类比在递归的此情况下,我们会将p
的下两位字符与s
比较:s[j]
与p[i+2]
);当p[i-1]=s[j] || p[i-1]!='.'
时,*
可能代表1次(多次),也可能代表0次,两种情况任一为真皆可。当*
代表0次时,如前,T[i][j]=T[i-2][j]
;当*
代表一次时,T[i][j]=T[i][j-1]
(类比在递归的此情况下,我们会将s
的下一位与p
进行匹配:s[j+1]
与p[i]
)。
根据以上分析,给出接下来T
的取值变化。
首先是初始化第-1列:
为了简便,一下以短横线代替箭头,表示取值的转移。
实现如下:
bool isMatch(string s, string p)
{
if (p.empty()) return s.empty();
if (p.size() == 1)return s.size() == 1 && (s[0] == p[0] || p[0] == '.');
int sdims = (int)s.size(), pdims = (int)p.size();
int i, j;
/* construct the table, and initialize */
char **maps = (char **)malloc((pdims+1) * sizeof(char *));
for (i = 0; i <= pdims; ++i)
maps[i] = (char *)malloc((sdims + 1) * sizeof(char));
maps[0][0] = 1;
for (i = 1; i <= pdims; ++i)
maps[i][0] = p[i - 1] == '*' && maps[i-2][0];
for (j = 1; j <= sdims; ++j)
maps[0][j] = 0;
/* run */
for (i = 1; i <= pdims; ++i)
for (j = 1; j <= sdims; ++j)
{
if (p[i - 1] != '*')
maps[i][j] = (p[i - 1] == s[j - 1] || p[i - 1] == '.') && maps[i - 1][j - 1];
else
maps[i][j] = maps[i - 2][j] || ((p[i - 2] == s[j - 1] || p[i - 2] == '.') && maps[i][j - 1]);
}
bool ret = maps[pdims][sdims] != 0;
/* deallocation */
for (i = 0; i <= pdims; ++i)
free(maps[i]);
free(maps);
return ret;
}
以上是本渣琢磨了一天的成果……编程太菜慢慢刷题中……勿喷么么哒
我的Github:https://github.com/Sissuire
第一手博客来源:https://sissuire.github.io/