简要贴下题目,具体详见这里:javascript
给定一个字符串 (s) 和一个字符模式 (p)。实现支持 '.' 和 '*' 的正则表达式匹配。html
'.' 匹配任意单个字符。'*' 匹配零个或多个前面的元素。java
匹配应该覆盖整个字符串 (s) ,而不是部分字符串。正则表达式
说明:算法
s 可能为空,且只包含从 a-z 的小写字母。p 可能为空,且只包含从 a-z 的小写字母,以及字符 . 和 *。express
示例 1:函数
输入:s = "aa"测试
p = "a"优化
输出: falsespa
解释: "a" 没法匹配 "aa" 整个字符串。
首先我在没有背景知识的状况下,素人想法,从字符串 s 第一个字符开始与正则中第一个 pattern 匹配,若是符合,则看第二个字符是否符合第一个 pattern,若是不符合则看下是否符合第二个 pattern。这样至关于有两个游标在字符串和正则上向后游动,不断匹配,当没法匹配上的时候就是不 match。
固然这是一个很基础的概念。由于题目中涉及 * 这个能够屡次匹配的通配符(其实题目已经很简化了),因此可能出现同一个字符字串匹配到多种 pattern 组合的状况。这就不仅仅是两个游标依次往下走的问题了。因此我决定先查下正则匹配的通用规则。
像上面提到的这个问题就涉及到回溯问题了,举个简单的例子:
字符串:abbbc正则:/ab{1,3}bbc/
匹配的过程是:
第6步因为c无法和b{1,3}
后面的b
匹配上,因此咱们把与b{1,3}
匹配上的bbb
吐出一位(也就是回退一位)拿这个b去匹配正则b{1,3}
后面的b
。这种因为往前走尝试一种路径(或者匹配规则)走不通,须要尝试另外一种路径的方式叫作回溯。在稍微复杂的正则中可能一次匹配的过程当中会涉及很是屡次的回溯。稍微详尽的例子看这里 。
按照初步掌握的知识先尝试写写看(会结合注释和伪代码):
// 主函数 function isMatch( s, p ) { }
我想了下因为在匹配的过程当中须要把整个正则拆分红小的子 pattern,那么我先把 p 分解了,省的游标一边向后走,一边还要判断解析正则。思路是把[a-z], ., [a-z], . 分别摘出来。
// 主函数 function isMatch( s, p ) { let patterns = toPatterns( p ); // 开始匹配 } function toPatterns( p ) { let result = []; if ( p.length===0 ) { return result; } for( let i = 0; i < p.length; ) { let currentP = p[ i ]; let nextP = p[ i+1 ]; if ( nextP!=='*' ) { // 单个字母 if ( currentP !== '.' && currentP !== '*' ) { result.push( { type: 'char', keyword: currentP } ); i++; continue; } // 单个. if ( currentP==='.' ) { result.push( { type: '.' } ); i++ continue; } // 单个* if ( currentP==='*' ) { throw 'invalid pattern'; } } else { if ( currentP==='.' ) { result.push( { type: '.*' } ); i += 2; continue; } else { result.push( { type: 'char*', keyword: currentP } ); i += 2; continue; } } } return result; }
而后开始循环判断:
// 主函数 function isMatch( s, p ) { let patterns = toPatterns( p ); // 开始匹配 /* 先判断边际条件 s 为空 p 为空 s 为空 p 为空 的状况,具体代码就省略了 */ let subPattern = patterns.shift(); let strIndex = 0; // 当 patterns 和 s 都轮询完了才算完结 while( subPattern || strIndex < s.length; ) { // 用字符和正则的子模式进行比较 if ( subPattern && subPattern.type==='char' s[strIndex]===subPattern.keyword ) { // 若是是 [a-z] 的正则,且匹配上了,那么字符串和正则都须要往下走一步: subPattern = patterns.shift(); strIndex++ } else if ( // 若是是 . 的正则匹配上了 ) { // 字符串和正则都须要往下走一步: subPattern = patterns.shift(); strIndex++ } else if ( // 若是是 [a-z]* 的正则匹配上了 ) { // 字符串下走一步,正则还能够用 strIndex++ } else if ( // 若是是 .* 的正则匹配上了 ) { // 字符串下走一步,正则还能够用 strIndex++ } else { // 若是没有匹配上,这里就开始考虑!!!回溯!!! } } } function toPatterns( p ) { // 省略 }
代码写到这里,我发现了个问题,若是要回溯,我要用很是多的变量记录各类状况,写不少分支条件,无法写啊。参考了别人的代码,发现把循环该成递归,能很好的解决这个问题(针对这道题目只有[a-z], .的状况会产生回溯):
var isMatch = function(s, p) { return isMatchImpl( s, toPatterns(p) ); }; function toPatterns( p ) { // 省略 } function isMatchImpl( s, patterns ) { // 开始匹配 /* 先判断边际条件 s 为空 patterns 为空 s 为空 patterns 为空 的状况,具体代码就省略了 */ let p = patterns[ 0 ]; if ( p.type==='char' && s[ 0 ]===p.keyword ) { return isMatchImpl( s.substr(1), patterns.slice(1) ); } else if ( p.type==='.' && s[ 0 ] ) { return isMatchImpl( s.substr(1), patterns.slice(1) ); } else if ( p.type==='char*' ) { if ( s[ 0 ]===p.keyword ) { // 这里经过逻辑或和递归,把回溯的各个条件依次执行 return isMatchImpl( s, patterns.slice(1) ) || isMatchImpl( s.substr(1), patterns ) || isMatchImpl( s.substr(1), patterns.slice(1) ); } else { // 这里经过逻辑或和递归,把回溯的各个条件依次执行 return isMatchImpl( s, patterns.slice(1) ) } } else if ( p.type==='.*' ) { if ( s ) { // 这里经过逻辑或和递归,把回溯的各个条件依次执行 return isMatchImpl( s, patterns.slice(1) ) || isMatchImpl( s.substr(1), patterns ) || isMatchImpl(s.substr(1), patterns.slice(1)); } else { // 这里经过逻辑或和递归,把回溯的各个条件依次执行 return isMatchImpl( s, patterns.slice(1) ); } } else { return false; } }
看下代码里面的注释,经过逻辑或的逻辑短路原则,结合递归,就把回溯的各个路径写成一行了。循环和递归真是好基友,各有各的适用场景。代码的功能完成了,经过了官方的测试用例。
代码完成了,可是执行效率颇有问题。看下上面讲回溯例子时候的图片,当回溯的时候若是 subpattern 没有变,且 strindex 没有变,那么结果是一致的,也就是说若是屡次执行能够被记录下来,不用每次都判断 subPattern
和 s[strIndex]
是否匹配。这个思路和优化斐波那契数列是否有点类似,就是对计算过的结果用空间换时间,对于相同的计算条件只须要计算一次。这个思路再加上这道题目,背后的原理其实就是动态规划的概念。
我简单说下什么叫动态规划:
这个好像和咱们正则匹配的过程不谋而合了:
而动态规划在算法中的应用,其一大优化策略就是充分利用前面保存的子问题的解来减小重复计算。因此改造下代码:
const P_TYPE_CHAR = 1; const P_TYPE_ANY_CHAR = 2; const P_TYPE_CHAR_ANY_TIME = 3; const P_TYPE_ANY_CHAR_ANY_TIME = 4; const Q_DOT = '.'; const Q_STAR = '*'; /** * @param {string} s * @param {string} p * @return {boolean} */ var isMatch = function(s, p) { return isMatchImpl( s, 0, toPatterns(p), 0 ); }; function toPatterns( p ) { // 省略 } let Cache = {}; function isMatchImpl( s, sIndex, patterns, pIndex ) { if ( sIndex===s.length && pIndex===patterns.length ) { return true; } if ( sIndex < s.length && pIndex===patterns.length ) { return false; } let cacheKey = `${sIndex}-${pIndex}`; if ( Cache[cacheKey]!==undefined ) { return Cache[cacheKey]; } let p = patterns[ pIndex ]; if ( p.type===P_TYPE_CHAR && s[ sIndex ]===p.keyword ) { Cache[ cacheKey ] = true; return isMatchImpl( s, ++sIndex, patterns, ++pIndex ); } else if ( p.type===P_TYPE_ANY_CHAR && s[ sIndex ] ) { Cache[ cacheKey ] = true; return isMatchImpl( s, ++sIndex, patterns, ++pIndex ); } else if ( p.type===P_TYPE_CHAR_ANY_TIME ) { Cache[ cacheKey ] = true; if ( s[ sIndex ]===p.keyword ) { return isMatchImpl( s, sIndex, patterns, ++pIndex ) || isMatchImpl( s, ++sIndex, patterns, pIndex ) || isMatchImpl( s, ++sIndex, patterns, ++pIndex ); } else { Cache[ cacheKey ] = false; return isMatchImpl( s, sIndex, patterns, ++pIndex ) } } else if ( p.type===P_TYPE_ANY_CHAR_ANY_TIME ) { Cache[ cacheKey ] = true; if ( s ) { return isMatchImpl( s, sIndex, patterns, ++pIndex ) || isMatchImpl( s, ++sIndex, patterns, pIndex ) || isMatchImpl(s, ++sIndex, patterns, ++pIndex ); } else { return isMatchImpl( s, sIndex, patterns, ++pIndex ); } } else { Cache[ cacheKey ] = false; return false; } }