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 == null || p == null) return false; int lenS = s.length(); int lenP = p.length(); char[] sChars = s.toCharArray(); char[] pChars = p.toCharArray(); boolean[][] dp = new boolean[lenS+1][lenP+1]; for (int i = 0; i <= lenS; i++) { for (int j = 0; j <= lenP; j++) { if (j == 0) { if (i == 0) { dp[i][j] = true; } // or is false continue; } if (i == 0) { // j != 0 if (j - 3 >= 0) { dp[i][j] = pChars[j-1] == '*' && dp[i][j-2]; } else { dp[i][j] = pChars[j-1] == '*'; // the first cant be * } continue; } if (sChars[i-1] == pChars[j-1] || pChars[j-1] == '.') { dp[i][j] = dp[i-1][j-1]; } else if (pChars[j-1] == '*') { if (pChars[j-2] == sChars[i-1] || pChars[j-2] == '.') { dp[i][j] = dp[i-1][j] || dp[i][j-2]; } else { dp[i][j] = dp[i][j-2]; } } } } return dp[lenS][lenP]; }
- 递归方法
和 dp 相同,递推的式子完全一样,只要注意递归出口就好。递归出口就是 dp 方法中的初始化部分,如果上面的 dp 方法懂了,递归方法就非常容易看懂,直接上代码。public boolean isMatch(String s, String p) { if (s == null || p == null) { return false; } return isMatch(s, p, s.length(), p.length()); } public boolean isMatch(String s, String p, int firstSLetters, int firstPLetters) { if (firstPLetters == 0) { if (firstSLetters == 0) { return true; } else { return false; } } if (firstSLetters == 0) { if (p.charAt(firstPLetters-1) == '*') { return isMatch(s, p, firstSLetters, firstPLetters-2); } else { return false; } } if (s.charAt(firstSLetters-1) == p.charAt(firstPLetters-1) || p.charAt(firstPLetters-1) == '.') { return isMatch(s, p, firstSLetters-1, firstPLetters-1); } else if (p.charAt(firstPLetters-1) == '*') { if (s.charAt(firstSLetters-1) == p.charAt(firstPLetters-2) || p.charAt(firstPLetters-2) == '.') { return isMatch(s, p, firstSLetters-1, firstPLetters) || isMatch(s, p, firstSLetters, firstPLetters-2); } else { return isMatch(s, p, firstSLetters, firstPLetters-2); } } return false; }