簡要貼下題目,具體詳見這裏: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; } }