LeetCode 正则表达式匹配
@author:Jingdai
@date:2020.11.28
题目描述(10题)
给你一个字符串 s
和一个字符规律 p
,请你来实现一个支持 '.'
和 '*'
的正则表达式匹配。
'.'
匹配任意单个字符'*'
匹配零个或多个前面的那一个元素
提示:
s
可能为空,且只包含从 a-z
的小写字母p
可能为空,且只包含从 a-z
的小写字母,以及字符 .
和 *
- 保证每次出现字符
*
时,前面都匹配到有效的字符
示例1
输入: s = "aa" p = "a*"
输出: true
示例2
输入: s = "ab" p = ".*"
输出: true
思路及代码
这个题目可以用动态规划或递归来解决,思路都差不多,这里先介绍 dp 的方法,然后介绍递归的方法。
-
dp 方法
首先要明确匹配的含义,这点很重要,这也是很多人第一次做的时候懵逼的原因(我就是)。匹配的含义是正则表达式 p
和字符串 s
以任何一种方式匹配就算匹配。比如字符串 "aa"
和 正则表达式 "a*a*"
,这里有好几种匹配方式,比如第一个 *
表示2个a,第二个 *
表示0 个 a;或者第一个 *
表示1个a,第二个 *
表示1个a。但要注意,只要有任何一种方式使正则表达式 p 和字符串 s 匹配了,那么就算是匹配了,而不用所有的方式都匹配。
接下来看 dp,如何定义状态呢?我们这里定义 dp[i][j]
表示字符串 s
的前 i
个字母和正则表达式 p
的前 j
个字母是否匹配(注意:i
,j
不是下标,后同)。
先看初始状态,首先很容易得到 dp[0][0]
为 true(s
的前0个字母和 p
的前 0 个字母);然后当 j
等于 0 时(p的前0个字母),只有当 i
等于0时 dp[i][j]
为true,否则都为false。但是当 i
等于 0 时,dp[i][j]
需要讨论。如图,当 p
为 "a*b*c"
时, dp[0][1]
显然为 false ,但是 dp[0][2]
为true(j = 2
的 * 代表前面有0个a); dp[0][3]
为 false, dp[0][4]
又是 true (j = 4
的 * 代表前面有0个a)。可以得出当 i
等于0时有: dp[i][j] = p.charAt(j-1) == '*' && dp[i][j-2]
。
下面就是 dp 的重点了,也是这个题的难点,状态转移方程。
从最简单的开始,如图,当 s
的第 i
个元素(不是下标,后同)和 p
的第 j 个元素(不是下标,后同)相同时(或 p
的第 j
个元素是 '.'
),那么很容易得出 dp[i][j] = dp[i-1][j-1]
。
当 s
的第 i
个元素和 p
的第 j
个元素不同且 p
的第 j
个元素为 '*'
时,又分为两种情况。
-
如图,当 s
的第 i
个元素和 p
的第 j-1
个元素不同时,那么只有将 p
的第 j-1
个元素和第 j
个元素去掉才有可能使它们相同,故此时 *
只能代表 0 个前面的元素,此时 dp[i][j] = dp[i][j-2]
。
-
当 s
的第 i
个元素和 p
的第 j-1
个元素相同时,如图,可以发现不管 i
前有几个相同的元素,如 "cba"
、"cbaa"
、"cbaaa"
,dp[i][j]
的结果总和 dp[i-1][j]
的结果相同,相当于这里的 i
和 j
都往前移动一位,j
移动代表 a*
少匹配一个 a ,不用动,故得到 dp[i][j] = dp[i-1][j]
。
但是还有特殊的情况,如图,例如当 s
为 "a"
, p 为 "aa*"
时,这时只能删除 a*
,才能使 s
和 p
匹配。此时 dp[i][j] = dp[i][j-2]
。前面说过,只要有一种方式匹配成功就算匹配,所以最后的递推式为 dp[i][j] = dp[i-1][j] || dp[i][j-2]
。
综上,所有情况的递推式为:
- 当
s
的第 i
个元素和 p
的第 j
个元素相同时:dp[i][j] = dp[i-1][j-1]
- 当
s
的第 i
个元素和 p
的第 j
个元素不同且 p
的第 j
个元素为 '*'
时: - 当
s
的第 i
个元素和 p
的第 j-1
个元素不同时:dp[i][j] = dp[i][j-2]
- 当
s
的第 i
个元素和 p
的第 j-1
个元素相同时:dp[i][j] = dp[i-1][j] || dp[i][j-2]
- 其他情况:
dp[i][j] = false
代码如下。
public boolean isMatch(String s, String p) {if (s &#61;&#61; null || p &#61;&#61; null)return false;int lenS &#61; s.length();int lenP &#61; p.length();char[] sChars &#61; s.toCharArray();char[] pChars &#61; p.toCharArray();boolean[][] dp &#61; new boolean[lenS&#43;1][lenP&#43;1];for (int i &#61; 0; i <&#61; lenS; i&#43;&#43;) {for (int j &#61; 0; j <&#61; lenP; j&#43;&#43;) {if (j &#61;&#61; 0) {if (i &#61;&#61; 0) {dp[i][j] &#61; true;} continue;}if (i &#61;&#61; 0) { if (j - 3 >&#61; 0) {dp[i][j] &#61; pChars[j-1] &#61;&#61; &#39;*&#39; && dp[i][j-2];} else {dp[i][j] &#61; pChars[j-1] &#61;&#61; &#39;*&#39;; }continue;}if (sChars[i-1] &#61;&#61; pChars[j-1] || pChars[j-1] &#61;&#61; &#39;.&#39;) {dp[i][j] &#61; dp[i-1][j-1];} else if (pChars[j-1] &#61;&#61; &#39;*&#39;) {if (pChars[j-2] &#61;&#61; sChars[i-1] || pChars[j-2] &#61;&#61; &#39;.&#39;) {dp[i][j] &#61; dp[i-1][j] || dp[i][j-2];} else {dp[i][j] &#61; dp[i][j-2];}}}}return dp[lenS][lenP];
}
- 递归方法
和 dp 相同&#xff0c;递推的式子完全一样&#xff0c;只要注意递归出口就好。递归出口就是 dp 方法中的初始化部分&#xff0c;如果上面的 dp 方法懂了&#xff0c;递归方法就非常容易看懂&#xff0c;直接上代码。
public boolean isMatch(String s, String p) {if (s &#61;&#61; null || p &#61;&#61; null) {return false;}return isMatch(s, p, s.length(), p.length());
}public boolean isMatch(String s, String p, int firstSLetters, int firstPLetters) {if (firstPLetters &#61;&#61; 0) {if (firstSLetters &#61;&#61; 0) {return true;} else {return false;}}if (firstSLetters &#61;&#61; 0) {if (p.charAt(firstPLetters-1) &#61;&#61; &#39;*&#39;) {return isMatch(s, p, firstSLetters, firstPLetters-2);} else {return false;}}if (s.charAt(firstSLetters-1) &#61;&#61; p.charAt(firstPLetters-1) || p.charAt(firstPLetters-1) &#61;&#61; &#39;.&#39;) {return isMatch(s, p, firstSLetters-1, firstPLetters-1);} else if (p.charAt(firstPLetters-1) &#61;&#61; &#39;*&#39;) {if (s.charAt(firstSLetters-1) &#61;&#61; p.charAt(firstPLetters-2) || p.charAt(firstPLetters-2) &#61;&#61; &#39;.&#39;) {return isMatch(s, p, firstSLetters-1, firstPLetters) || isMatch(s, p, firstSLetters, firstPLetters-2);} else {return isMatch(s, p, firstSLetters, firstPLetters-2);}} return false;
}