给你一个字符串 s 和一个字符规律 p,请你来实现一个支持 '.' 和 '*' 的正则表达式匹配。java
'.' 匹配任意单个字符 '*' 匹配零个或多个前面的那一个元素 所谓匹配,是要涵盖 整个 字符串 s的,而不是部分字符串。正则表达式
说明:less
s 可能为空,且只包含从 a-z 的小写字母。函数
p 可能为空,且只包含从 a-z 的小写字母,以及字符 . 和 *。性能
拿到题目的第一反应就是这是一个 regex 表达式解析引擎,可是过于复杂。spa
因而能够按照必定的顺序去实现。code
下面来逐步看一下这个题目的解答过程。blog
public boolean isMatch(String s, String p) { return s.matches(p); }
Runtime: 64 ms, faster than 24.57% of Java online submissions for Regular Expression Matching. Memory Usage: 40.3 MB, less than 7.95% of Java online submissions for Regular Expression Matching.
虽然实现了,可是对于咱们我的基本没有任何收益。递归
也没有体会到 regex 解析过程的快乐,并且性能也不怎么样。rem
若是 p 中没有任何 *
号,那么对比起来其实比较简单,就是文本 s 和 p 一一对应。
.
对应任意单个字符,也不难。
若是存在 *
,这个问题就会难那么一些。
public boolean isMatch(String s, String p) { // 若是 p 已经遍历结束,直接看 s 是否结束。 if(p.isEmpty()) { return s.isEmpty(); } // 第一位是否匹配判断 boolean firstMatch = !s.isEmpty() && (s.charAt(0) == p.charAt(0) || p.charAt(0) == '.'); if(p.length() >= 2 && p.charAt(1) == '*') { // 1. 第一位匹配 && 后续匹配 (* 一次或者屡次) // 2. c* 出现零次,则直接全文本匹配。 return (firstMatch && isMatch(s.substring(1), p)) || isMatch(s, p.substring(2)); } else { // 第二位不是 *,则直接跳过第一位看后续的信息。 return firstMatch && isMatch(s.substring(1), p.substring(1)); } }
这个用到了递归,性能以下:
Runtime: 88 ms, faster than 8.51% of Java online submissions for Regular Expression Matching. Memory Usage: 39.8 MB, less than 27.13% of Java online submissions for Regular Expression Matching.
一个字,惨~
你也许发现了,原来的代码中
isMatch(s.substring(1), p.substring(1))
这种相似的匹配结果,其实是一次次的在重复的。
好比第一次咱们匹配 [1, 10],后续又匹配 [2, 10]
这样若是你学过 DP 那么会有一个想法,可否重复利用已经判断过的内容呢?
DP 无敌。
咱们用递归中一样的回溯方法,除此以外,由于函数 match(text[i:], pattern[j:])
只会被调用一次,咱们用 dp(i, j) 来应对剩余相同参数的函数调用,这帮助咱们节省了字符串创建操做所须要的时间,也让咱们能够将中间结果进行保存。
这里的核心区别就是不对 text/pattern 作 substring 的操做,而是从前日后处理。
enum Result { TRUE, FALSE } class Solution { Result[][] memo; public boolean isMatch(String text, String pattern) { memo = new Result[text.length() + 1][pattern.length() + 1]; return dp(0, 0, text, pattern); } public boolean dp(int i, int j, String text, String pattern) { if (memo[i][j] != null) { return memo[i][j] == Result.TRUE; } boolean ans; if (j == pattern.length()){ // 若是 pattern 已经遍历结束 ans = i == text.length(); } else{ // 第一位的判断和原来相似 boolean first_match = (i < text.length() && (pattern.charAt(j) == text.charAt(i) || pattern.charAt(j) == '.')); if (j + 1 < pattern.length() && pattern.charAt(j+1) == '*'){ ans = (dp(i, j+2, text, pattern) || first_match && dp(i+1, j, text, pattern)); } else { ans = first_match && dp(i+1, j+1, text, pattern); } } // 保存临时结果 memo[i][j] = ans ? Result.TRUE : Result.FALSE; return ans; } }
Runtime: 3 ms, faster than 83.97% of Java online submissions for Regular Expression Matching. Memory Usage: 39.9 MB, less than 22.69% of Java online submissions for Regular Expression Matching.
性能仍是不错的。
实际上这个性能是比实现一个 regex 引擎要好的,由于 regex 的编译构建 DFA/NFA 很是的耗时。
理解了上面的解法,下面的解法就比较简单了。
从后往前处理,这样就避免掉了默认值的问题,不须要像上面同样引入一个奇奇怪怪的枚举值。
public boolean isMatch(String s, String p) { //dp 存放的是后面处理的结果 boolean[][] dp = new boolean[s.length()+1][p.length()+1]; dp[s.length()][p.length()] = true; for(int i = s.length(); i >= 0; i--) { for(int j = p.length()-1; j >= 0; j--) { // 核心代码保持不变 // 这里不用判断是否为 empty 的问题 boolean firstMatch = i < s.length() && (s.charAt(i) == p.charAt(j) || p.charAt(j) == '.'); // 判断星号 if(j+1 < p.length() && p.charAt(j+1) == '*') { // 出现零次 // 一次或者屡次 dp[i][j] = dp[i][j+2] || (firstMatch && dp[i+1][j]); } else { dp[i][j] = firstMatch && dp[i+1][j+1]; } } } // 直接返回结果 return dp[0][0]; }
还算比较优雅,性能还算满意。
Runtime: 2 ms, faster than 92.84% of Java online submissions for Regular Expression Matching. Memory Usage: 38.3 MB, less than 73.31% of Java online submissions for Regular Expression Matching.
虽然咱们使用过不少次 Regex 正则表达式,可是实际上实现起来可能没有使用那么简单。
后续有机会咱们能够讲述写如何实现一个相对完整的正则表达式引擎。