LC 10. 正则表达式匹配
1. 问题描述
给你一个字符串 s
和一个字符规律 p
,请你来实现一个支持 '.'
和 '*'
的正则表达式匹配。
'.'
匹配任意单个字符'*'
匹配零个或多个前面的那一个元素
所谓匹配,是要涵盖 整个 字符串 s的,而不是部分字符串。
说明:
s
可能为空,且只包含从a-z
的小写字母。p
可能为空,且只包含从a-z
的小写字母,以及字符.
和*
。
示例 1 输入: s = "aa" p = "a" 输出: false 解释: "a" 无法匹配 "aa" 整个字符串。
示例 2 输入: s = "aa" p = "a*" 输出: true 解释: 因为 '*' 代表可以匹配零个或多个前面的那一个元素, 在这里前面的元素就是 'a'。 因此,字符串 "aa" 可被视为 'a' 重复了一次。
示例 3 输入: s = "ab" p = ".*" 输出: true 解释: ".*" 表示可匹配零个或多个('*')任意字符('.')。
示例 4 输入: s = "aab" p = "c*a*b" 输出: true 解释: 因为 '*' 表示零个或多个,这里 'c' 为 0 个, 'a' 被重复一次。因此可以匹配字符串 "aab"。
示例 5 输入: s = "mississippi" p = "mis*is*p*." 输出: false
Related Topics 字符串 动态规划 回溯算法
2. 题解
方法一、动态规划
题目中的匹配是一个「逐步匹配」的过程:每次从字符串 p 中取出一个字符或者「字符 + 星号」的组合,并在 s 中进行匹配。对于 p 中一个字符而言,它只能在 s 中匹配一个字符,匹配的方法具有唯一性;而对于 p 中「字符 + 星号」的组合而言,它可以在 s 中匹配任意自然数个字符,并不具有唯一性。因此可以考虑使用动态规划,对匹配的方案进行枚举。
用 f[i][j] 表示 s 的前 i 个字符与 p 中的前 j 个字符是否能够匹配。在进行状态转移时,考虑 p 的第 j 个字符的匹配情况:


细节:动态规划的边界条件为 f[0][0]=true,即两个空字符串是可以匹配的。最终的答案即为 f[m][n],其中 m 和 n 分别是字符串 s 和 p 的长度。
来源:https://leetcode-cn.com/u/newhar/ 以一个例子详解动态规划转移方程: S = abbbbc P = ab*d*c 1. 当 i, j 指向的字符均为字母(或 '.' 可以看成一个特殊的字母)时, 只需判断对应位置的字符即可, 若相等,只需判断 i,j 之前的字符串是否匹配即可,转化为子问题 f[i-1][j-1]. 若不等,则当前的 i,j 肯定不能匹配,为 false. f[i-1][j-1] i | | S [a b b b b][c] P [a b * d *][c] | j 2. 如果当前 j 指向的字符为 '*',则不妨把类似 'a*', 'b*' 等的当成整体看待。 看下面的例子 i | S a b [b] b b c P a [b *] d * c | j 注意到当 'b*' 匹配完 'b' 之后,它仍然可以继续发挥作用。 因此可以只把 i 前移一位,而不丢弃 'b*', 转化为子问题 f[i-1][j]: i | <-- S a [b] b b b c P a [b *] d * c | j 另外,也可以选择让 'b*' 不再进行匹配,把 'b*' 丢弃。 转化为子问题 f[i][j-2]: i | S a b [b] b b c P [a] b * d * c | j <-- 3. 冗余的状态转移不会影响答案, 因为当 j 指向 'b*' 中的 'b' 时, 这个状态对于答案是没有用的, 原因参见评论区 稳中求胜 的解释, 当 j 指向 '*' 时, dp[i][j]只与dp[i][j-2]有关, 跳过了 dp[i][j-1].
代码
class Solution { public boolean isMatch(String s, String p) { int m = s.length(); int n = p.length(); //该数组默认的元素是false //用 f[i][j] 表示 s 的前 i 个字符与 p 中的前 j 个字符是否能够匹配 boolean[][] f = new boolean[m + 1][n + 1]; f[0][0] = true; //i 为 0,j 不为 0。需要判断,可能存在匹配。因为 * 可以消掉一个字符。 //i 不为 0,j 为 0。则一定不匹配。因此从 j=1 开始判断。 //i=1 表示 s 的第一个字符,其在字符串中的索引对应着0! for(int i=0; i<=m; i++){ for(int j=1; j<=n; j++){ if(p.charAt(j - 1)=='*'){ f[i][j] = f[i][j-2]; if(matches(s, p, i, j-1)){ f[i][j] = f[i][j] || f[i-1][j]; } }else{ if(matches(s, p, i, j)){ f[i][j] = f[i-1][j-1]; } } } } return f[m][n]; } public boolean matches(String s, String p, int i, int j) { //双for循环i从0开始而j从1开始,所以此处只需要判断i的值是否合法。 if (i == 0) { return false; } if (p.charAt(j - 1) == '.') { return true; } return s.charAt(i - 1) == p.charAt(j - 1); } }
复杂度分析
-
时间复杂度:O(mn),其中 m 和 n 分别是字符串 s 和 p 的长度。需要计算出所有的状态,并且每个状态在进行转移时的时间复杂度为 O(1)。
-
空间复杂度:O(mn),即为存储所有状态使用的空间。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 【译】Visual Studio 中新的强大生产力特性
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 【设计模式】告别冗长if-else语句:使用策略模式优化代码结构