版权声明: 本文为博主Bravo Yeung(知乎UserName同名)的原创文章,欲转载请先私信获博主允许,转载时请附上网址

http://blog.csdn.net/lzuacm



C#版 - Leetcode 10. 正则表达式匹配 - 题解

LeetCode 10. Regular Expression Matching

在线提交

https://leetcode.com/problems/regular-expression-matching/

题目描述


给定一个字符串 (s) 和一个字符模式pattern (p)。实现支持 '.' 和'*' 的正则表达式匹配。

'.' 匹配任意单个字符。
'*' 匹配零个或多个前面的元素。

匹配应该覆盖整个字符串 (s) ,而不是部分字符串。

说明:

  • s 可能为空,且只包含从 a-z 的小写字母。
  • p 可能为空,且只包含从 a-z 的小写字母,以及字符 '.' 和'*'。

示例 1:

输入:
s = "aa"
p = "a"
输出: false
解释: "a" 无法匹配 "aa" 整个字符串。

示例 2:

输入:
s = "aa"
p = "a*"
输出: true
解释: '*' 代表可匹配零个或多个前面的元素, 即可以匹配 'a' 。因此, 重复 'a' 一次, 字符串可变为 "aa"。

示例 3:

输入:
s = "ab"
p = ".*"
输出: true
解释: ".*" 表示可匹配零个或多个('*')任意字符('.')。

示例 4:

输入:
s = "aab"
p = "c*a*b"
输出: true
解释: 'c' 可以不被重复, 'a' 可以被重复一次。因此可以匹配字符串 "aab"。

示例 5:

输入:
s = "mississippi"
p = "mis*is*p*."
输出: false

  ●  题目难度: Hard

分析

首先,需要提及一个概念 - 克莱尼星号Kleene Star。

克莱尼星号(算子)

Kleene 星号算子,或称Kleene 闭包,德语称Kleensche Hülle,在数学上是一种适用于字符串或符号及字元的集合的一元运算,通常被称为自由幺半群结构(free monoid construction)。当 Kleene 星号算子被应用在一个集合V" role="presentation">VV 时,写法是 V∗" role="presentation">V∗V∗。它被广泛用于正则表达式,正则表达式由Stephen Kleene引入以描述某些自动机的特征,其中*表示“零或更多”次。

如果V" role="presentation">VV 是一组字符串,则 V∗" role="presentation">V∗V∗被定义为包含空字符串ϵ" role="presentation">ϵϵ的V" role="presentation">VV 的最小超集,并在字符串连接操作下闭合。

如果V" role="presentation">VV 是一组符号或字符,则 V∗" role="presentation">V∗V∗是V" role="presentation">VV 中符号上所有字符串的集合,包括空字符串ϵ" role="presentation">ϵϵ。

集合 V∗" role="presentation">V∗V∗也可以描述为可以通过连接V" role="presentation">VV的任意元素生成的有限长度字符串集合,允许多次使用相同的元素。 如果V" role="presentation">VV 是空集ϕ" role="presentation">ϕϕ或单子集ϵ" role="presentation">ϵϵ,则V∗={ϵ}" role="presentation">V∗={ϵ}V∗={ϵ}; 如果V" role="presentation">VV 是任何其他有限集,则 V∗" role="presentation">V∗V∗是可数无限集。 该算子用于生成语法或重写规则。

定义及标记法

假定

V0={ϵ}" role="presentation">V0={ϵ}V0={ϵ}, 其中ϵ" role="presentation">ϵϵ是空字符串。

递归的定义集合

Vi+1={wv:w∈Vi∧v∈V}" role="presentation">Vi+1={wv:w∈Vi∧v∈V}Vi+1={wv:w∈Vi∧v∈V}, 这里的 i>0" role="presentation">i>0i>0,

如果V" role="presentation">VV是一个形式语言,集合V" role="presentation">VV的第 i" role="presentation">ii次幂是集合 V" role="presentation">VV 同自身的 i 次串接的简写。就是说,Vi" role="presentation">ViVi可以被理解为是从 V" role="presentation">VV 中的符号形成的所有长度为 i" role="presentation">ii 的字符串的集合。

所以在 V" role="presentation">VV上的 Kleene 星号运算的定义是 V∗=⋃i=0+∞Vi={ε}∪V∪V2∪V3∪…" role="presentation">V∗=⋃+∞i=0Vi={ε}∪V∪V2∪V3∪…V∗=⋃i=0+∞Vi={ε}∪V∪V2∪V3∪…。就是说,它是从V" role="presentation">VV中的符号生成的所有可能的有限长度的字符串的搜集。

例子

Kleene 星号算子应用于字符串集合的例子:

{“ab”, “c”}* = {ε, “ab”, “c”, “abab”, “abc”, “cab”, “cc”, “ababab”, “ababc”, “abcab”, “abcc”, “cabab”, “cabc”, “ccab”, “ccc”, …}

Kleene 星号应用于字元集合的例子:

{‘a’, ‘b’, ‘c’}* = {ε, “a”, “b”, “c”, “aa”, “ab”, “ac”, “ba”, “bb”, “bc”, …}

推广

Kleene 星号经常推广到任何幺半群 (M, ∘" role="presentation">∘∘),也就是,一个集合 M 和在 M 上的二元运算 ∘" role="presentation">∘∘ 有着:

  • (闭包) ∀a,b∈M: a∘b∈M" role="presentation">∀a,b∈M: a∘b∈M∀a,b∈M: a∘b∈M

  • (结合律) ∀a,b,c∈M: (a∘b)∘c=a∘(b∘c)" role="presentation">∀a,b,c∈M: (a∘b)∘c=a∘(b∘c)∀a,b,c∈M: (a∘b)∘c=a∘(b∘c)

  • (单位元) ∃ϵ∈M: ∀a∈M: a∘ϵ=a=ϵ∘a" role="presentation">∃ϵ∈M: ∀a∈M: a∘ϵ=a=ϵ∘a∃ϵ∈M: ∀a∈M: a∘ϵ=a=ϵ∘a

如果 VM 的子集,则V" role="presentation">VV被定义为包含ϵ" role="presentation">ϵϵ(空字符串)并闭合于这个运算下的 V 的最小超集。接着V" role="presentation">VV自身是幺半群,并被称为“V" role="presentation">VV生成的自由幺半群”。这是上面讨论的 Kleene 星号的推广,因为在某个符号的集合上所有字符串的集合形成了一个幺半群(带有字符串串接作为二元运算)。


方法1:递归

如果没有Kleene星号(正则表达式的 * 通配符),问题会更容易一些 - 我们只需从左到右检查text的每个字符是否与模式pattern匹配。

当存在*时,我们可能需要检查text的许多不同后缀,看它们是否与模式pattern的其余部分匹配。 递归解法是表示这种关系的直接方法。

算法

如果没有Kleene星号,相应的Python代码将如下:

def match(text, pattern):
if not pattern: return not text
first_match = bool(text) and pattern[0] in {text[0], '.'}
return first_match and match(text[1:], pattern[1:])

如pattern中存在*,则它将处于第二位置 pattern[1] 。 然后,我们可以忽略模式pattern的这一部分,或删除text中的匹配字符。 如果在任何这些操作之后我们在剩余的字符串上能匹配上,则初始输入是匹配的。相应的Java代码如下:

class Solution {
public boolean isMatch(String text, String pattern) {
if (pattern.isEmpty()) return text.isEmpty();
boolean first_match = (!text.isEmpty() &&
(pattern.charAt(0) == text.charAt(0) || pattern.charAt(0) == '.')); if (pattern.length() >= 2 && pattern.charAt(1) == '*'){
return (isMatch(text, pattern.substring(2)) ||
(first_match && isMatch(text.substring(1), pattern)));
} else {
return first_match && isMatch(text.substring(1), pattern.substring(1));
}
}
}

复杂度分析

- 时间复杂度:将text和模式pattern的长度分别记作 T" role="presentation">TT, P" role="presentation">PP。 在最坏的情况下,调用match(text[i:], pattern[2j:])的次数将为 (i+ji)" role="presentation">(i+ji)(i+ji) ,将产生的字符串的时间复杂度阶数为 O(T−i)" role="presentation">O(T−i)O(T−i)和 O(P−2⋅j)" role="presentation">O(P−2⋅j)O(P−2⋅j) 。 因此,时间复杂度可表示为 ∑i=0T∑j=0P/2(i+ji)O(T+P−i−2j)" role="presentation">∑Ti=0∑P/2j=0(i+ji)O(T+P−i−2j)∑i=0T∑j=0P/2(i+ji)O(T+P−i−2j)。 通过本文之外的一些努力,可证明这个复杂度可规约为 O((T+P)2T+P2)" role="presentation">O((T+P)2T+P2)O((T+P)2T+P2)。

- 空间复杂度:对于每次的match调用,我们将创建上述的字符串,可能会创建重复项。 如果没有释放内存,这将总共需要的空间为O((T+P)2T+P2)" role="presentation">O((T+P)2T+P2)O((T+P)2T+P2),即使实际上必需的不一样 P" role="presentation">PP和 T" role="presentation">TT的后缀所占空间仅为O(T2+P2)" role="presentation">O(T2+P2)O(T2+P2) 。


方法2:动态规划

由于该问题具有最优子结构 ,因此缓存中间结果是很自然的。 我们探索如何表示dp(i, j) :text[i:]和pattern[j:] 能否匹配上? 我们可以使用较短字符串的问题的解来表示当前字符串的解。

算法

我们继续进行与方法1相同的递归,除非因为调用只会用到match(text[i:], pattern[j:]) ,我们才使用dp(i, j) 来处理这些调用,省去了代价很高的字符串构建操作,且允许我们缓存中间结果。用Java实现的代码如下:

自底向上的方式(归纳法):

class Solution {
public boolean isMatch(String text, String pattern) {
boolean[][] dp = new boolean[text.length() + 1][pattern.length() + 1];
dp[text.length()][pattern.length()] = true; for (int i = text.length(); i >= 0; i--){
for (int j = pattern.length() - 1; j >= 0; j--){
boolean first_match = (i < text.length() &&
(pattern.charAt(j) == text.charAt(i) ||
pattern.charAt(j) == '.'));
if (j + 1 < pattern.length() && pattern.charAt(j+1) == '*'){
dp[i][j] = dp[i][j+2] || first_match && dp[i+1][j];
} else {
dp[i][j] = first_match && dp[i+1][j+1];
}
}
}
return dp[0][0];
}
}

自顶向下的方式(演绎法):

enum Result {
TRUE, FALSE
} class Solution {
Result[][] memo; public boolean isMatch(String text, String pattern) {
memo = new Result[text.length() + 1][pattern.length() + 1];
return dp(0, 0, text, pattern);
} public boolean dp(int i, int j, String text, String pattern) {
if (memo[i][j] != null) {
return memo[i][j] == Result.TRUE;
}
boolean ans;
if (j == pattern.length()){
ans = i == text.length();
} else{
boolean first_match = (i < text.length() &&
(pattern.charAt(j) == text.charAt(i) ||
pattern.charAt(j) == '.')); if (j + 1 < pattern.length() && pattern.charAt(j+1) == '*'){
ans = (dp(i, j+2, text, pattern) ||
first_match && dp(i+1, j, text, pattern));
} else {
ans = first_match && dp(i+1, j+1, text, pattern);
}
}
memo[i][j] = ans ? Result.TRUE : Result.FALSE;
return ans;
}
}

自底向上的分析,是从具体到抽象,比如 已知数学公式,基于公式来coding,属于演绎法;自顶向下的分析,是从抽象到具体,属于归纳法。

自底向上

自底向上就是已经知道了所有递归边界,把所有可能的状态都算出来。基本步骤是一个拓扑排序的过程,从所有递归边界出发,当一个状态被所有可能的下层状态更新后,就用这个状态去更新后面的状态。直到所求的状态被彻底更新完成为止。

通俗地讲就是:从初始已知的状态出发,向外拓展,最后到达目标状态。

自顶向下:

自顶向下就是不考虑整个树结构,直接从要求的状态开始展开式子,如果式子中的某个状态的值还不清楚,就递归的从这个状态展开。递归结束后式子中的状态都被对应的值替换了,所求状态自然也就清楚了。

通俗地讲就是:从最终状态开始,找到可以到达当前状态的状态,如果该状态还没处理,就先处理该状态。

复杂度分析

  • 时间复杂度:将text和模式pattern的长度分别记作 T" role="presentation">TT, P" role="presentation">PP。

    每次从 i=0,⋯,T;j=0,...,P" role="presentation">i=0,⋯,T;j=0,...,Pi=0,⋯,T;j=0,...,P范围内dp(i, j)的调用工作做完一次,所花的时间为O(1)" role="presentation">O(1)O(1)。因此,时间复杂度是 O(T⋅P)" role="presentation">O(T⋅P)O(T⋅P)。

  • 空间复杂度:该算法中使用的内存空间即为布尔值的缓存,占用的空间大小为O(T⋅P)" role="presentation">O(T⋅P)O(T⋅P)。 因此,空间复杂度是 O(T⋅P)" role="presentation">O(T⋅P)O(T⋅P) 。



    Reference:

    Regular Expression Matching - LeetCode Articles

    Kleene星号

Leetcode 10. 正则表达式匹配 - 题解的更多相关文章

  1. Java实现 LeetCode 10 正则表达式匹配

    10. 正则表达式匹配 给你一个字符串 s 和一个字符规律 p,请你来实现一个支持 '.' 和 '*' 的正则表达式匹配. '.' 匹配任意单个字符 '*' 匹配零个或多个前面的那一个元素 所谓匹配, ...

  2. [LeetCode] 10. 正则表达式匹配

    题目链接:https://leetcode-cn.com/problems/regular-expression-matching/ 题目描述: 给定一个字符串 (s) 和一个字符模式 (p).实现支 ...

  3. LeetCode 10. 正则表达式匹配(Regular Expression Matching)

    题目描述 给定一个字符串 (s) 和一个字符模式 (p).实现支持 '.' 和 '*' 的正则表达式匹配. '.' 匹配任意单个字符. '*' 匹配零个或多个前面的元素. 匹配应该覆盖整个字符串 (s ...

  4. LeetCode 10——正则表达式匹配

    1. 题目 2. 解答 在 回溯算法 中我们介绍了一种递归的思路来求解这个问题. 此外,这个问题也可以用动态规划的思路来解决.我们定义状态 \(P[i][j]\) 为子串 \(s[0, i)\) 和 ...

  5. leetcode题目10.正则表达式匹配(困难)

    题目描述: 给你一个字符串 s 和一个字符规律 p,请你来实现一个支持 '.' 和 '*' 的正则表达式匹配. '.' 匹配任意单个字符'*' 匹配零个或多个前面的那一个元素所谓匹配,是要涵盖 整个  ...

  6. 【LeetCode】正则表达式匹配(动态规划)

    题目描述 给定一个字符串 (s) 和一个字符模式 (p).实现支持 '.' 和 '*' 的正则表达式匹配. '.' 匹配任意单个字符. '*' 匹配零个或多个前面的元素. 匹配应该覆盖整个字符串 (s ...

  7. LeetCode10. 正则表达式匹配

    10. 正则表达式匹配 描述 给定一个字符串 (s) 和一个字符模式 (p).实现支持 '.' 和 '*' 的正则表达式匹配. '.' 匹配任意单个字符. '*' 匹配零个或多个前面的元素. 匹配应该 ...

  8. leetcode 10 Regular Expression Matching(简单正则表达式匹配)

    最近代码写的少了,而leetcode一直想做一个python,c/c++解题报告的专题,c/c++一直是我非常喜欢的,c语言编程练习的重要性体现在linux内核编程以及一些大公司算法上机的要求,pyt ...

  9. Leetcode(10)正则表达式匹配

    Leetcode(10)正则表达式匹配 [题目表述]: 给定一个字符串 (s) 和一个字符模式 (p).实现支持 '.' 和 '*' 的正则表达式匹配. '.' 匹配任意单个字符. '*' 匹配零个或 ...

随机推荐

  1. 按键精灵PC版<末日王者>

    更新模式[强制] 更新版本[1.28] 更新链接[https://yunfei-1256035889.cos.ap-beijing.myqcloud.com/%E6%9C%AB%E6%97%A5%E4 ...

  2. Fiddler和app抓包

    1:请在“运行”,即下面这个地方输入certmgr.msc并回车,打开证书管理. 打开后,请点击操作--查找证书,如下所示: 然后输入“fiddler”查找所有相关证书,如下所示: 可以看到,我们找到 ...

  3. 爬虫之selenium和PhantomJS

    ---恢复内容开始--- selenium selenium是什么? 是Python的一个第三方库,对外提供的接口可以操作浏览器,然后让浏览器完成自动化的操作 环境搭建 .安装: pip instal ...

  4. Java中 StringTokenizer 的用法

    一.StringTokenizer 1.1 StringTokenizer简介及其构造函数的三种形式: StringTokenizer类是字符串分割解析类型,其属于java.util包,在使用之前需要 ...

  5. CF76A.Gift [最小生成树]

    CF76A.Gift 题意:noi2014魔法森林弱化版QwQ,最小化\(max(g_i)*G + max(s_i)*S\)的最小生成树 考虑按g升序加边,用已在生成树中的边和新加入的边求当前最小生成 ...

  6. 安装JAVA jdk

      下载软件包,jdk-10.0.1_windows-x64_bin 根据提示一步步安装.安装完成之后,配置环境. 控制面板——>系统——>编辑系统变量 ​ 4. 系统变量 增加path变 ...

  7. Fio测试工具参数

    以随机读为例:fio -ioengine=libaio -group_reporting -direct=1 -name=testsda -numjobs=1 --time_based --runti ...

  8. JS获取键盘事件

    <script type="text/javascript" language=JavaScript charset="UTF-8"> docume ...

  9. entOS7查看开放端口命令

    CentOS7的开放关闭查看端口都是用防火墙来控制的,具体命令如下: 查看已经开放的端口: firewall-cmd --list-ports 开启端口 firewall-cmd --zone=/tc ...

  10. C++ STL编程轻松入门【转载】

    1 初识STL:解答一些疑问 1.1 一个最关心的问题:什么是STL "什么是STL?",假如你对STL还知之甚少,那么我想,你一定很想知道这个问题的答案,坦率地讲,要指望用短短数 ...