LeetCode - 10 正则表达式匹配

目录

题目来源

题目描述

示例

提示

题目解析

算法源码


题目来源

10. 正则表达式匹配 - 力扣(LeetCode)

题目描述

给你一个字符串 s 和一个字符规律 p,请你来实现一个支持 '.' 和 '*' 的正则表达式匹配。

'.' 匹配任意单个字符
'*' 匹配零个或多个前面的那一个元素
所谓匹配,是要涵盖 整个 字符串 s的,而不是部分字符串。

示例

示例 1

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

示例 2

  • 输入:s = "aa", p = "a*"
  • 输出:true
  • 解释:因为 '*' 代表可以匹配零个或多个前面的那一个元素, 在这里前面的元素就是 'a'。因此,字符串 "aa" 可被视为 'a' 重复了一次。

示例 3

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

提示

  • 1 <= s.length <= 20
  • 1 <= p.length <= 20
  • s 只包含从 a-z 的小写字母。
  • p 只包含从 a-z 的小写字母,以及字符 . 和 *。
  • 保证每次出现字符 * 时,前面都匹配到有效的字符

题目解析

本题如果用正则表达式实现的话,逻辑非常简单,代码实现也非常简单,如下代码所示:

Java正则解法

import java.util.regex.Pattern;class Solution {public boolean isMatch(String s, String reg) {return Pattern.compile("^" + reg + "$").matcher(s).find();}
}

  

JS正则解法

/*** @param {string} s* @param {string} p* @return {boolean}*/
var isMatch = function(s, p) {return new RegExp(`^${p}$`).test(s);
};

Python正则解法

import re
class Solution(object):def isMatch(self, s, p):return re.compile("^"+ p +"$").match(s) is not None

但是,我们可以发现正则匹配的性能是非常差的,并且这题的目的是想让我们实现类似于正则匹配的功能,而不是让我们去套皮。

本题的最优解法是动态规划。

字符串s,正则p,现在想要确定正则p是否可以匹配字符串s。

我们假设 dp[ i ][ j ] 表示字符串s的0 ~ i部分,和正则p的0 ~ j 部分的匹配结果:

  • 要么是true,即匹配
  • 要么是false,即不匹配

那么此时,dp[ i ][ j ] 其实可以分情况讨论:


如果 p[ j ] == '*' 的话,

我们需要注意的是 '*' 在正则是一个量词符,用于表示它前面一个字符可能出现0次或多次。 

比如p = "abc*",表示 '*' 前面的字符c可能出现0次或多次,即 p 可以匹配:

  • "ab":c出现0次
  • "abc":出现1次
  • "abcccccc":c出现多次

那么我们不仅需要关注p[ j ],还需要关注 p [ j - 1 ],因为p[ j ]是量词符,而p[ j - 1 ]才是实际要匹配的内容字符。

比如上面例子p = "abc*"中,p[ j - 1 ] = 'c',p[ j ] = '*',而p[ j - 1 ] + p[ j ] 可以匹配字符串s最后部分的0个'c',或者1个‘c’,或者多个'c'。

如果 p[ j - 1 ] != s[ i ] 的话,

那么p[ j - 1 ] + p[ j ] 就只能匹配 s 最后的0个字符,比如例子:p = "abc*",s="ab"

如果 p[ j - 1 ] == s[ i ] 但是 p[ j - 1 ] != s[ i - 1 ],

那么p[ j - 1 ] + p[ j ]就只能匹配 s 最后的1个字符,比如例子:p = "abc*",s="abc"

如果 p[ j - 1 ] == s[ i ] 且 p[ j - 1 ] == s[ i - 1 ] 但是 p[ j - 1 ] != s[ i - 2 ],

那么p[ j - 1 ] + p[ j ]就只能匹配 s 最后的2个字符,比如例子:p = "abc*",s="abcc"

.............

如果 p[ j - 1 ] == s[ i ] 且 .... 且  p[ j - 1 ] == s[ i - k ] 但是 p [ j - 1 ] != s[ i - k - 1 ]

那么p[ j - 1 ] + p[ j ]就只能匹配 s 最后的k个字符

因此,基于上面逻辑写状态转义方程的话,有

if p[ j ] == '*':

        dp[ i ][ j ] =

                     ( not_eq(p[ j - 1 ], s[ i ])   and  dp[ i ][ j - 2 ] )

                     or

                     ( eq(p[ j - 1 ], s[ i ])  and  not_eq(p[ j - 1 ], s[ i - 1 ])  and dp[ i - 1 ][ j - 2 ] )

                     or

                     ( eq(p[ j - 1 ], s[ i ])  and  eq(p[ j - 1 ], s[ i - 1 ])  and  not_eq(p[ j - 1 ], s[ i - 2 ]) and dp[ i - 2 ][ j - 2 ] )

                     or

                     ........

可以发现,这个状态转义方程是写不完的。因此需要简化,而dp[i][j]的简化,依赖于dp[i-1][j],下面是dp[i-1][j]的状态转义方程

if p[ j ] == '*':

        dp[ i - 1 ][ j ] =

                     ( not_eq(p[ j - 1 ], s[ i - 1 ])   and  dp[ i - 1 ][ j - 2 ] )

                     or

                     ( eq(p[ j - 1 ], s[ i -1 ])  and  not_eq(p[ j - 1 ], s[ i - 2 ])  and dp[ i - 2 ][ j - 2 ] )

                     or

                     ( eq(p[ j - 1 ], s[ i - 1 ])  and  eq(p[ j - 1 ], s[ i - 2 ])  and  not_eq(p[ j - 1 ], s[ i - 3 ]) and dp[ i - 3 ][ j - 2 ] )

                     or

                     ........

对比,dp[i][j] 和 dp[i-1][j],可以发现,dp[i][j]的部分内容可以转化为dp[i-1][j]

 即可得状态转义方程:

if p[ j ] == '*':

        dp[ i ][ j ] = ( not_eq(p[ j - 1 ], s[ i ]) and dp[ i ][ j - 2 ] )  or  ( eq(p[ j - 1 ], s[ i ]) and dp[ i - 1 ][ j ] ) 

但是上面状态转义方程是存在问题的!!!!!

如果p[j] == '*':

  1. 如果 p[j-1] != s[i],那么 dp[i][j] 就可以分解为 dp[i][j-2]子问题,即正则p的j-1~j部分不匹配字符串s的任何内容,接下来只需要关心字符串s的0~i部分是否可以被正则p的0~j-2部分匹配。
  2. 如果 p[j-1] == s[i],那么dp[i][j] 就可以分解为 dp[i-1][j] 子问题,因为 p[j-1] == s[i],所以我们只需要关心字符串s的0~i-1部分是否可以被正则p的0~j匹配即可。

但是,实际上关于1,即使p[j-1] == s[i]的情况下,dp[i][j]也可以分解为dp[i][j-2]子问题,什么意思呢?

  1. 如果 p[j-1] == s[i],那么正则p的j-1~j部分依旧可以选择不匹配字符串s的任何内容,接下来如果字符串s的0~i部分是否可以被正则p的0~j-2部分匹配,那么依然可以说明s可以被p匹配。

因此,最正确的状态转义方程是:

if p[ j ] == '*':

        dp[ i ][ j ] = ( dp[ i ][ j - 2 ] )  or  ( eq(p[ j - 1 ], s[ i ]) and dp[ i - 1 ][ j ] ) 

另外关于,上面eq和not_eq该如何实现呢?

首先,如果 p[ j - 1 ] == s[ i ]的话,那么肯定是相同的,eq(p[ j - 1 ], s[ i ]) == true

其次,在正则表达式中,有一个通配符‘.’,它可以匹配任意一个实际字符,因此如果 p[ j - 1 ] == '.' 的话,则也可以认为 p[ j - 1 ] == s[ i ],即eq(p[ j - 1 ], s[ i ]) == true


如果 p[ j ] ! = '*' 的话,

那么此时的判断就非常简单了,因为p[ j ]确定不是量词符了,因此p[ j ]就只能匹配一个字符s[ i ]

如果 eq(p[ j ], s[ i ]),那么 dp[i][j] = dp[i-1][j-1],否则直接dp[i][j] = false


因此综合来看,dp[i][j]的状态转义方程如下:

if p[ j ] == '*':
        dp[ i ][ j ] = ( not_eq(p[ j - 1 ], s[ i ]) and dp[ i ][ j - 2 ] )  or  ( eq(p[ j - 1 ], s[ i ]) and dp[ i - 1 ][ j ] ) 
else:
        dp[ i ][ j ] = eq(p[ j ], s[ i ]) ? dp[i - 1][j - 1] : false

当然,上面状态转义方程需要注意索引越界处理,必须要保证索引不越界。


关于,dp[i][j] 的初始化问题,比如dp[0][0]应该初始化为多少?

dp[0][0]的含义是 字符串s的0~0部分,是否可以被正则p的0~0部分匹配?

似乎说匹配也可以,不匹配也可以,因为正则为空。

因此,为了初始化容易理解,我们应该为s,p开头都加一个" ",即

s = " " + s

p = " " + p

那么其实原来dp[0][0]的问题,就变为dp[1][1]的问题,即字符串s的0~1部分,是否可以被正则p的0~1部分匹配,即" "是否可以被" "匹配,那么这里是肯定确定可以匹配的。

但是我们不能初始化dp[1][1] = true,而是需要初始化dp[0][0] = true。具体原因,大家可以基于上面状态转义方程,结合几个例子验证一下。

Java算法源码

class Solution {public boolean isMatch(String s, String p) {s = " " + s;p = " " + p;int n = s.length();int m = p.length();boolean[][] dp = new boolean[n][m];dp[0][0] = true;for (int i = 0; i < n; i++) {// 内层循环遍历的是正则p的范围,如果j=0.那么代表正则为空,此时匹配结果必然为false,而dp数组初始化时所有元素都初始化为了false,因此这里j可以从1开始for (int j = 1; j < m; j++) {if (p.charAt(j) == '*') {// 注意下面case1和case2仅为了方便理解,所以分开写了,实际时可以代入到case1 || case2中boolean case1 = j >= 2 && dp[i][j - 2];boolean case2 = eq(p.charAt(j - 1), s.charAt(i)) && i >= 1 && dp[i - 1][j];dp[i][j] = case1 || case2;} else {dp[i][j] = eq(p.charAt(j), s.charAt(i)) && i >= 1 && dp[i - 1][j - 1];}}}return dp[n - 1][m - 1];}public static boolean eq(char p, char s) {return p == s || p == '.';}
}

JS算法源码

/*** @param {string} s* @param {string} p* @return {boolean}*/
var isMatch = function (s, p) {s = " " + s;p = " " + p;const n = s.length;const m = p.length;const dp = new Array(n).fill(0).map(() => new Array(m).fill(false));dp[0][0] = true;for (let i = 0; i < n; i++) {// 内层循环遍历的是正则p的范围,如果j=0.那么代表正则为空,此时匹配结果必然为false,而dp数组初始化时所有元素都初始化为了false,因此这里j可以从1开始for (let j = 1; j < m; j++) {if (p[j] == "*") {// 注意下面case1和case2仅为了方便理解,所以分开写了,实际时可以代入到case1 || case2中const case1 = j >= 2 && dp[i][j - 2];const case2 = eq(p[j - 1], s[i]) && i >= 1 && dp[i - 1][j];dp[i][j] = case1 || case2;} else {dp[i][j] = eq(p[j], s[i]) && i >= 1 && dp[i - 1][j - 1];}}}return dp[n - 1][m - 1];
};function eq(p, s) {return p == s || p == ".";
}

Python算法源码

class Solution(object):def isMatch(self, s, p):s = " " + sp = " " + pn = len(s)m = len(p)dp = [[False] * m for _ in range(n)]dp[0][0] = Truefor i in range(n):# 内层循环遍历的是正则p的范围,如果j=0.那么代表正则为空,此时匹配结果必然为false,而dp数组初始化时所有元素都初始化为了false,因此这里j可以从1开始for j in range(1, m):if p[j] == '*':# 注意下面case1和case2仅为了方便理解,所以分开写了,实际时可以代入到case1 || case2中case1 = j >= 2 and dp[i][j - 2]case2 = self.eq(p[j - 1], s[i]) and i >= 1 and dp[i - 1][j]dp[i][j] = case1 or case2else:dp[i][j] = self.eq(p[j], s[i]) and i >= 1 and dp[i - 1][j - 1]return dp[n - 1][m - 1]def eq(self, p, s):return p == s or p == '.'

  

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://xiahunao.cn/news/254712.html

如若内容造成侵权/违法违规/事实不符,请联系瞎胡闹网进行投诉反馈,一经查实,立即删除!

相关文章

感恩计算机专业作文,感恩作文(精选10篇)

感恩作文(精选10篇) 在学习、工作或生活中&#xff0c;大家都经常接触到作文吧&#xff0c;借助作文人们可以实现文化交流的目的。怎么写作文才能避免踩雷呢&#xff1f;下面是小编整理的感恩作文(精选10篇)&#xff0c;希望对大家有所帮助。 感恩作文1 父母&#xff0c;是世界…

一岁一礼,感恩相遇 | 中创员工生日会

生日快乐happy birthday to you 一岁一礼&#xff0c;承载着祝福 纸短情长&#xff0c;记录着温暖 中创算力2022开年 第一场生日会如约而至 11位中创寿星 共赴一场生日庆宴 享受专属于寿星们的幸福时光&#xff01; HAPPY BIRTHDAY 生日party 生活需要仪式感 每一个特…

生日快乐网站_【总结 】文化自信源自基层贺房氏网站建站十五周年

重要提醒&#xff1a;2004年起创建房氏网(房氏网站fang.org.cn)及QQ群&#xff0c;收集房氏家谱、源流、名人、企业&#xff0c;搭建寻根、联谊等一体文化平台&#xff0c;专业专注于房氏文化&#xff0c;为全球100多万房家人服务的一个综合体&#xff0c;欢迎大家的加入与参与…

一个生日微信小程序 生日动画_生日当天发朋友圈的文案 生日快乐微信小句子...

1.承蒙时光不弃&#xff0c;终究又长大了一岁&#xff0c;感谢每个阶段不同的自己。 2.希望我以后的人生平安喜乐&#xff0c;得偿所愿。 3.又长大了一岁&#xff0c;要更勇敢&#xff0c;少发脾气&#xff0c;按时睡觉&#xff0c;不要乱想。 4.要长大&#xff0c;要乖&#x…

用python写生日快乐说说_生日快乐的说说(精选50句)

生日快乐的说说(精选50句) 随着移动互联网和社交网络的发展&#xff0c;越来越多人喜欢在朋友圈上发布说说&#xff0c;用以分享自己当日的心情和优美的句子。还在苦苦寻找个性、独特的说说吗&#xff1f;以下是小编精心整理的生日快乐的说说(精选50句)&#xff0c;欢迎大家借鉴…

flex 布局的基本概念 - 详解

flex 布局的基本概念 Flexible Box 模型&#xff0c;通常被称为 flexbox&#xff0c;是一种一维的布局模型。它给 flexbox 的子元素之间提供了强大的空间分布和对齐能力。本文给出了 flexbox 的主要特性&#xff0c;更多的细节将在别的文档中探索。我们说 flexbox 是一种一维的…

祝妈妈生日快乐的html的代码,祝妈妈生日快乐的朋友圈说说 祝妈妈生日快乐的说说句子...

妈妈是这个世界上伟大而又平凡的人物,给了我们生命,同时承担教育之责,很少会有机会和妈妈说一些暖心的话,所以在妈妈生日这一天,一定要祝妈妈生日快乐,下面八宝网小编就带来祝妈妈生日快乐的朋友圈说说,祝妈妈生日快乐的说说句子。 祝妈妈生日快乐的朋友圈说说 妈妈,今天…

生日祝福html_更新,礼包选择,头像框及太子生日金币活动

幻之试炼2&#xff1a; 全人物解锁教学 ||| 全通关方法总结 |||全隐藏关卡、bug总结 本周攻略&#xff1a; 百忍大战攻略 ||| C忍-忍者新秀止水教学 ||| B忍六尾人柱力羽高教学 ||| 截至10/5日&#xff0c;微信QQ全礼包 >>>本周快报活动更新 忍法帖新忍者—金加银角 1…

23岁生日感言:坚持到成功的那刻为止

子健说 王子健的创业之路 作者 l 王子健 来源 l 子健说&#xff08;ID&#xff1a;zijianshuo&#xff09; 转载请联系授权&#xff08;微信ID&#xff1a;WZJ455&#xff09; 今天是2019年5月3日&#xff0c;我已经正式步入了23岁。 美美地睡了一个懒觉&#xff0c;舒服地泡…

高晓松50岁生日感言:可感恩的很多,可原谅的很少

点击“技术领导力”关注∆ 每天早上8:30推送 作者 l 文七君 来源 l 粥左罗&#xff08;ID&#xff1a;fangdushe520&#xff09; 2019年11月14日&#xff0c;高晓松50岁。 凌晨时分&#xff0c;他发了张敷着面膜的自拍。下午三点&#xff0c;又补发了一篇长文。 高晓松说过&a…

【C++】string类的模拟实现

目录 1.默认成员函数1.1 构造函数1.2 拷贝构造1.2.1 传统写法1.2.2 现代写法 1.3 赋值构造1.3.1 传统写法1.3.2 现代写法 1.4 析构函数 2.其他成员函数2.1 迭代器2.2 容量操作2.2.1 size()2.2.2 capacity()2.2.3 reserve()2.2.4 resize()2.2.5 clear() 2.3 访问操作&#xff08…

一文读懂TSC时钟: (x86_64/arm64)实现介绍和编程使用

Linux(16)之Time Stamp Counter Author&#xff1a;Once Day Date&#xff1a;2023年5月30日 参考文档: 4. Environment Abstraction Layer — Data Plane Development Kit 23.03.0 documentation (dpdk.org)DPDK: lib/eal/include/generic/rte_cycles.h File Reference测量…

反射之使用自定义注解并处理自定义注解

注解&#xff1a;说起注解初学者可能不太明白&#xff0c;annotation是jdk1.5 引入的新特性&#xff0c;中文名叫注解&#xff0c;它提供了一种安全的类似注释的机制&#xff0c;用来将任何的信息或元数据&#xff08;metadata&#xff09;与程序元素&#xff08;类、方法、成员…

java自定义注解解析及自定义注解

jdk1.5之后提供了注解&#xff08;Annotation&#xff09;这一种语法。其主要作用是编译检查&#xff08;比如override&#xff09;和代码分析&#xff08;通过代码中添加注解&#xff0c;利用注解解析器对添加了注解的代码进行分析&#xff0c;获取想要的结果&#xff0c;一般…

自定义注解开发

自定义注解的语法要求&#xff1a; Target({ElementType.METHOD,ElementType.TYPE}) Retention(RetentionPolicy.RUNTIME) Inherited Documented public interface Description {String desc();String author();int age() default 18; } 首先我们要明确这不是一个接口&#x…

自定义注解(上)自定义注解的定义和检查

什么是注解&#xff1f; 注解是接口的一种变型&#xff0c;定义一组属性&#xff0c;让类、方法或属性用标记的方式调用这些属性。不仅是带有一些属性和值&#xff0c;某些注解带有一些特殊的功能。 如单元测试Test&#xff0c;可以让方法不依赖主函数单独运行&#xff0c;右…

自定义注解详解

文章引用 深入理解Java&#xff1a;注解&#xff08;Annotation&#xff09;自定义注解入门 自定义注解详细介绍 说在最前 文章忽略特性的一些基本概念 注解的本质是反射 1. 自定义注解 注解其实就是一种标记&#xff0c;可以在程序代码中的关键节点&#xff08;类、方法、…

查看 HTTP 请求的数据.

文章结构 如果是 GET 请求如果是 POST 请求方法1&#xff1a;DEBUG 窗口&#xff08;**爽、超级爽、吴迪爽**&#xff09;&#xff1a;方法2&#xff1a;写方法读取流中数据&#xff08;繁琐&#xff0c;难用&#xff09;&#xff1a; 我们可能会碰到 MVC 拿不到前端的参数&…

基于Html5的在线资料库的设计与实现(asp.NET,SQLServer)

在线资料库系统采用.NET开发平台进行开发&#xff0c;开发工具采用Microsoft Visual Studio 2010集成开发环境&#xff0c;后台编程语言采用C#编程语言来进行编程开发&#xff0c;数据库我们采用当下流行的SQL Server 2008数据库管理系统来存放平台中的数据信息&#xff0c;整个…

【软硬件测试】测试经验:软硬件结合测试要点

目录 一、应用行业 二、测试要点 三、硬件测试 &#xff08;1&#xff09;测试含义 &#xff08;2&#xff09;测试方法 &#xff08;3&#xff09;相关链接 四、结合测试 &#xff08;1&#xff09;测试含义 &#xff08;2&#xff09;测试工具 &#xff08;3&am…