10. 正则表达式匹配
给你一个字符串 s 和一个字符规律 p,请你来实现一个支持 ‘.’ 和 ‘*’ 的正则表达式匹配。
1 2
| '.' 匹配任意单个字符 '*' 匹配零个或多个前面的那一个元素
|
所谓匹配,是要涵盖 整个 字符串 s的,而不是部分字符串。
说明:
- s 可能为空,且只包含从 a-z 的小写字母。
- p 可能为空,且只包含从 a-z 的小写字母,以及字符 . 和 *。
示例 1:
1 2 3 4 5
| 输入: s = "aa" p = "a" 输出: false 解释: "a" 无法匹配 "aa" 整个字符串。
|
示例 2:
1 2 3 4 5
| 输入: s = "aa" p = "a*" 输出: true 解释: 因为 '*' 代表可以匹配零个或多个前面的那一个元素, 在这里前面的元素就是 'a'。因此,字符串 "aa" 可被视为 'a' 重复了一次。
|
示例 3:
1 2 3 4 5
| 输入: s = "ab" p = ".*" 输出: true 解释: ".*" 表示可匹配零个或多个('*')任意字符('.')。
|
示例 4:
1 2 3 4 5
| 输入: s = "aab" p = "c*a*b" 输出: true 解释: 因为 '*' 表示零个或多个,这里 'c' 为 0 个, 'a' 被重复一次。因此可以匹配字符串 "aab"。
|
示例 5:
1 2 3 4
| 输入: s = "mississippi" p = "mis*is*p*." 输出: false
|
思路:
可以使用动态规划来解此题。
首先定义状态,有过求LCS和LIC的经验,我们很轻易类似的写出该题的状态定义:
1
| dp[i][j] 串S以i结尾,串P以j结尾是否能够匹配。
|
我们考虑状态转移方程:
先分类考虑:
第一种情况显然是false。
第二种情况是说明可以匹配,所以由S前i-1,P前j-1匹配的情况决定。
第三种情况把’.’看做s[i]可以和第二种情况合并
重点是第四种情况,p[j]为’*’ 的情况:
单独一个‘ ’是不构成语义的,所以出现‘ ’就要看前一个字符,而前面有可能出现的又有‘.’和字母两种情况。
前面说过了,我们遇见‘.’直接把他当作与s[i]相等的字符即可。不需要做特殊处理。
所以就只剩下了‘*’ 前面是字母的
而分成的两种情况:
第一种情况由于’*’可以代表零个之前的字符。例如:
是可以匹配的。
所以我们遇到’*’前面的字母和s[i-1]不匹配就可以跳过这两个字符。即
最后’*’前面的字母和s[i-1]匹配:
那么就有按照‘*’的语义,又需要划分三种情况:
- *加上前面的字符不与s[i-1]匹配。
- 加上前面的字符只与s[i-1]匹配 即 作废。
- *加上前面的字符可以与多个s[i-1]匹配。
前两种情况其实与我们之前分析的不加‘*’的情况基本一致。
那么与多个字符匹配怎么转移呢?
我们确定匹配多个字符可以一个一个来,只要dp[i-1][j]可以匹配。那么只要[j]是,则dp[i][j]一定可以匹配上。(别忘了,我们的前提条件是 和之前的字母匹配)
1 2 3 4 5 6 7 8
| ###b ###b* 匹配。
###bb ###b* 一定能匹配。
|
至此我们就将所有情况分析完毕。
代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| class Solution { public: bool isMatch(string s, string p) { int n = s.size(), m = p.size(); vector<vector<int>> dp(n+1,vector<int>(m+1)); dp[0][0] = 1; //处理 aa 和c*c*c*aa 匹配的情况。前缀可以忽略。 for(int i = 1; i <= m; i++){ if(i-2 >= 0 && dp[0][i-2] && p[i-1] == '*') dp[0][i] = 1; } for(int i = 1; i <= n; i++){ for(int j = 1; j <= m; j++){ //单个字符匹配。 if(s[i-1] == p[j-1] || (p[j-1] == '.')){ dp[i][j] = dp[i-1][j-1]; //单个字符不匹配 }else if(p[j-1] == '*'){ // baa bc*aa 形式 if(p[j-2] != s[i-1] && p[j-2] != '.'){ dp[i][j] = dp[i][j-2]; }else{ // 否则是前一个字符可以匹配 // 三种情况, // 忽略这个匹配的字符,dp[i][j] = dp[i][j-2]; // 只匹配这个字符 dp[i][j] = dp[i][j-1]; // 匹配多个字符,即看i之前的字符和当前能不能匹配上,dp[i][j] = dp[i-1][j]; // ###b 和 ###b* 如果能匹配上, ###bb 和 ###b* 也能匹配上。 dp[i][j] = (dp[i][j-2] || dp[i][j-1] || dp[i-1][j]); } } } } return dp[n][m]; } };
|
这里有一个点必须初始化,因为下标是从1开始的,无法访问到0,当遇到处理 aa 和c c c* aa 匹配的情况时,是需要访问到0的位置的,所以需要预处理。前缀可以为空的情况。