本文我们来看看LeetCode 第10题:正则表达式匹配的解析过程。
文章目录
一、引言
各位算法爱好者们,今天我们要探讨的是LeetCode第10题——正则表达式匹配。这道题目不仅是面试中的常客,更是编程中处理字符串问题的一大难点。相信很多人一看到“正则表达式”这几个字就头疼得不行。没关系,今天我们将用幽默风趣的方式,逐步破解这个“洪荒之力”。
题目描述
给你一个字符串
s
和一个字符模式p
,请你实现一个支持 ‘.’ 和 ‘*’ 的正则表达式匹配。‘.’ 匹配任意单个字符。
‘*’ 匹配零个或多个前面的那一个元素。所谓匹配,是要涵盖整个字符串
s
,而不是部分字符串。
示例
示例 1:
输入: s = "aa" p = "a"
输出: false
解释: "a" 无法匹配 "aa" 整个字符串。
示例 2:
输入: s = "aa" p = "a*"
输出: true
解释: '*' 可以匹配零个或多个前面的那一个元素,在这里前面的元素是 'a'。
示例 3:
输入: s = "ab" p = ".*"
输出: true
解释: ".*" 表示可匹配零个或多个('*')任意字符('.')。
示例 4:
输入: s = "aab" p = "c*a*b"
输出: true
解释: 'c' 可以不被重复,'a' 可以被重复一次。因此可以匹配字符串 "aab"。
示例 5:
输入: s = "mississippi" p = "mis*is*p*."
输出: false
好啦,以上是题目的描述。接下来,我们开始解密这个题目吧!
二、解题思路
要搞定这个正则表达式匹配,我们可以借助动态规划。动态规划这个大杀器不仅能处理最短路径问题,还能帮我们解决这种复杂的字符串匹配问题。
动态规划概述
动态规划的核心思想是将问题分解成子问题,并存储子问题的解以避免重复计算。对于本题来说,我们可以定义一个二维数组 dp
,其中 dp[i][j]
表示字符串 s
的前 i
个字符和模式 p
的前 j
个字符是否匹配。
动态规划状态转移方程
我们要搞清楚以下几点:
- 当
p[j]
是普通字符时,dp[i][j]
取决于dp[i-1][j-1]
和s[i-1] == p[j-1]
。 - 当
p[j]
是.
时,dp[i][j]
取决于dp[i-1][j-1]
,因为.
可以匹配任意字符。 - 当
p[j]
是*
时,情况稍微复杂一点,因为*
可以匹配零个或多个前面的字符。
状态转移方程分析
- 普通字符匹配:当
p[j-1]
是普通字符且s[i-1] == p[j-1]
时,当前字符匹配成功,dp[i][j]
的值取决于dp[i-1][j-1]
。 .
匹配:当p[j-1]
是.
时,它可以匹配任意一个字符,当前字符匹配成功,dp[i][j]
的值同样取决于dp[i-1][j-1]
。*
匹配:当p[j-1]
是*
时,它可以匹配零个或多个前面的字符。- 如果
*
代表匹配零个字符,我们可以忽略这个*
和它前面的字符,即dp[i][j] = dp[i][j-2]
。 - 如果
*
代表匹配一个或多个字符,我们需要检查s[i-1]
是否等于p[j-2]
,或者p[j-2]
是否是.
。如果是,那么dp[i][j]
取决于dp[i-1][j]
。
- 如果
初始状态
dp[0][0] = true
:表示空字符串和空模式是匹配的。- 当模式是空的情况下,只有在字符串也是空的情况下才能匹配,因此
dp[i][0] = false
,对于所有i > 0
。 - 当字符串是空的情况下,模式需要能匹配空字符串。即对于模式
p
来说,只有形如a*b*c*
这样的模式能匹配空字符串。因此dp[0][j]
的值取决于p[j-1]
是否是*
,如果是,那么dp[0][j] = dp[0][j-2]
。
思路流程图
为了更加清晰地展示解题思路,我们使用mermaid绘制流程图:
三、Java代码实现
下面是完整的Java代码实现:
public class Solution {
public boolean isMatch(String s, String p) {
int m = s.length();
int n = p.length();
boolean[][] dp = new boolean[m + 1][n + 1];
dp[0][0] = true;
// 初始化 dp 数组,处理空字符串和模式匹配
for (int j = 1; j <= n; j++) {
if (p.charAt(j - 1) == '*') {
dp[0][j] = dp[0][j - 2];
}
}
// 填充 dp 数组
for (int i = 1; i <= m; i++) {
for (int j = 1; j <= n; j++) {
if (p.charAt(j - 1) == s.charAt(i - 1) || p.charAt(j - 1) == '.') {
dp[i][j] = dp[i - 1][j - 1];
} else if (p.charAt(j - 1) == '*') {
dp[i][j] = dp[i][j - 2];
if (p.charAt(j - 2) == s.charAt(i - 1) || p.charAt(j - 2) == '.') {
dp[i][j] = dp[i][j] || dp[i - 1][j];
}
}
}
}
return dp[m][n];
}
}
代码解析
-
初始化二维数组
dp
:int m = s.length(); int n = p.length(); boolean[][] dp = new boolean[m + 1][n + 1]; dp[0][0] = true;
我们首先初始化一个二维数组
dp
,大小为(m+1) x (n+1)
,并将dp[0][0]
设为true
,表示空字符串和空模式是匹配的。 -
处理空字符串和模式匹配:
for (int j = 1; j <= n; j++) { if (p.charAt(j - 1) == '*') { dp[0][j] = dp[0][j - 2]; } }
当模式中的字符是
*
时,dp[0][j]
取决于dp[0][j-2]
,因为*
可以匹配零个前面的字符。 -
填充
dp
数组:for (int i = 1; i <= m; i++) { for (int j = 1; j <= n; j++) { if (p.charAt(j - 1) == s.charAt(i - 1) || p.charAt(j - 1) == '.') { dp[i][j] = dp[i - 1][j - 1]; } else if (p.charAt(j - 1) == '*') { dp[i][j] = dp[i][j - 2]; if (p.charAt(j - 2) == s.charAt(i - 1) || p.charAt(j - 2) == '.') { dp[i][j] = dp[i][j] || dp[i - 1][j]; } } } }
我们通过双重循环遍历字符串
s
和模式p
,根据前面分析的状态转移方程填充dp
数组。- 当
p[j-1]
是普通字符或.
时,dp[i][j]
取决于dp[i-1][j-1]
。 - 当
p[j-1]
是*
时,dp[i][j]
取决于dp[i][j-2]
或dp[i-1][j]
,具体取决于前一个字符是否匹配。
- 当
-
返回最终结果:
return dp[m][n];
最终返回
dp[m][n]
,表示字符串s
和模式p
是否匹配。
四、举几个栗子
栗子一:普通匹配
输入: s = "aa", p = "a"
- 初始化
dp
数组:dp = [[true, false], [false, false], [false, false]]
- 填充
dp
数组:dp = [[true, false], [false, true], [false, false]]
- 返回结果:
false
栗子二:带 *
匹配
输入: s = "aa", p = "a*"
- 初始化
dp
数组:dp = [[true, false, true], [false, false, false], [false, false, false]]
- 填充
dp
数组:dp = [[true,false, true], [false, true, true], [false, false, true]]
- 返回结果:
true
栗子三:带 .
和 *
的匹配
输入: s = "ab", p = ".*"
- 初始化
dp
数组:dp = [[true, false, true], [false, false, false], [false, false, false]]
- 填充
dp
数组:dp = [[true, false, true], [false, true, true], [false, false, true]]
- 返回结果:
true
栗子四:复杂匹配
输入: s = "aab", p = "c*a*b"
- 初始化
dp
数组:dp = [[true, false, false, false, false, false], [false, false, false, false, false, false], [false, false, false, false, false, false], [false, false, false, false, false, false]]
- 填充
dp
数组:dp = [[true, false, true, false, true, false], [false, false, true, true, true, false], [false, false, true, false, true, true], [false, false, false, false, true, true]]
- 返回结果:
true
栗子五:匹配失败
输入: s = "mississippi", p = "mis*is*p*."
- 初始化
dp
数组:dp = [[true, false, false, false, false, false, false, false, false], [false, false, false, false, false, false, false, false, false], [false, false, false, false, false, false, false, false, false], [false, false, false, false, false, false, false, false, false], [false, false, false, false, false, false, false, false, false], [false, false, false, false, false, false, false, false, false], [false, false, false, false, false, false, false, false, false], [false, false, false, false, false, false, false, false, false], [false, false, false, false, false, false, false, false, false], [false, false, false, false, false, false, false, false, false], [false, false, false, false, false, false, false, false, false], [false, false, false, false, false, false, false, false, false]]
- 填充
dp
数组:dp = [[true, false, false, false, false, false, false, false, false], [false, true, false, false, true, false, true, false, false], [false, false, true, false, true, false, true, false, false], [false, false, false, true, true, false, true, false, false], [false, false, false, false, true, false, true, true, false], [false, false, false, false, false, true, true, true, false], [false, false, false, false, false, false, true, false, false], [false, false, false, false, false, false, false, false, false], [false, false, false, false, false, false, false, false, false], [false, false, false, false, false, false, false, false, false], [false, false, false, false, false, false, false, false, false], [false, false, false, false, false, false, false, false, false]]
- 返回结果:
false
五、总结
通过这个题目,我们不仅学习了如何使用动态规划解决正则表达式匹配问题,还练习了如何处理复杂的状态转移方程。正则表达式匹配问题看似复杂,但只要掌握了动态规划的思想,就能游刃有余地解决。
如果本文对您有所帮助的话,请收藏文章、关注作者、订阅专栏,感激不尽。