leetcode算法第10題正則表達(dá)式匹配-回溯-動態(tài)規(guī)劃

題目

簡要貼下題目与涡,具體詳見這里

給定一個(gè)字符串 (s) 和一個(gè)字符模式 (p)。實(shí)現(xiàn)支持 '.' 和 '*' 的正則表達(dá)式匹配。

'.' 匹配任意單個(gè)字符。

'*' 匹配零個(gè)或多個(gè)前面的元素解取。

匹配應(yīng)該覆蓋整個(gè)字符串 (s) ,而不是部分字符串返顺。

說明:

s 可能為空禀苦,且只包含從 a-z 的小寫字母。

p 可能為空遂鹊,且只包含從 a-z 的小寫字母振乏,以及字符 . 和 *。

示例 1:

輸入:

s = "aa"

p = "a"

輸出: false

解釋: "a" 無法匹配 "aa" 整個(gè)字符串秉扑。

解題過程

初始想法

首先我在沒有背景知識的情況下慧邮,素人想法,從字符串 s 第一個(gè)字符開始與正則中第一個(gè) pattern 匹配邻储,如果符合赋咽,則看第二個(gè)字符是否符合第一個(gè) pattern旧噪,如果不符合則看下是否符合第二個(gè) pattern吨娜。這樣相當(dāng)于有兩個(gè)游標(biāo)在字符串和正則上向后游動,不斷匹配淘钟,當(dāng)無法匹配上的時(shí)候就是不 match宦赠。

回溯

當(dāng)然這是一個(gè)很基礎(chǔ)的概念。因?yàn)轭}目中涉及 * 這個(gè)可以多次匹配的通配符(其實(shí)題目已經(jīng)很簡化了)米母,所以可能出現(xiàn)同一個(gè)字符字串匹配到多種 pattern 組合的情況勾扭。這就不單單是兩個(gè)游標(biāo)依次往下走的問題了。所以我決定先查下正則匹配的通用規(guī)則铁瞒。

像上面提到的這個(gè)問題就涉及到回溯問題了妙色,舉個(gè)簡單的例子:

字符串:abbbc

正則:/ab{1,3}bbc/

匹配的過程是:


leetcode算法第10題正則表達(dá)式匹配-回溯-動態(tài)規(guī)劃圖1.png

第6步由于c沒法和b{1,3}后面的b匹配上,所以我們把與b{1,3}匹配上的bbb吐出一位(也就是回退一位)拿這個(gè)b去匹配正則b{1,3}后面的b慧耍。這種由于往前走嘗試一種路徑(或者匹配規(guī)則)走不通身辨,需要嘗試另一種路徑的方式叫做回溯。在稍微復(fù)雜的正則中可能一次匹配的過程中會涉及非常多次的回溯芍碧。稍微詳盡的例子看這里 煌珊。

代碼

按照初步掌握的知識先嘗試寫寫看(會結(jié)合注釋和偽代碼):

// 主函數(shù)
function isMatch( s, p ) {

}

我想了下由于在匹配的過程中需要把整個(gè)正則拆分成小的子 pattern,那么我先把 p 分解了泌豆,省的游標(biāo)一邊向后走定庵,一邊還要判斷解析正則。思路是把[a-z], ., [a-z], . 分別摘出來。


// 主函數(shù)
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!=='*' ) {
      // 單個(gè)字母
      if (
        currentP !== '.' &&
        currentP !== '*'
      ) {
        result.push( {
          type: 'char',
          keyword: currentP
        } );
        i++;
        continue;
      }
      // 單個(gè).
      if ( currentP==='.' ) {
        result.push( {
          type: '.'
        } );
        i++
        continue;
      }
      // 單個(gè)* 
      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;
}

然后開始循環(huán)判斷:

// 主函數(shù)
function isMatch( s, p ) {
  let patterns = toPatterns( p );
  // 開始匹配
  /* 
  先判斷邊際條件
    s 為空 p 為空
    s 為空
    p 為空
  的情況蔬浙,具體代碼就省略了
  */
  let subPattern = patterns.shift();
  let strIndex = 0;

  // 當(dāng) patterns 和 s 都輪詢完了才算完結(jié)
  while(
    subPattern ||
    strIndex < s.length;
  ) {
    // 用字符和正則的子模式進(jìn)行比較
    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 {
      // 如果沒有匹配上,這里就開始考慮R锘巍C弁佟!回溯JT唷!
    }
  }


}

function toPatterns( p ) {
  // 省略
}

代碼寫到這里咱揍,我發(fā)現(xiàn)了個(gè)問題颖榜,如果要回溯,我要用非常多的變量記錄各種情況煤裙,寫很多分支條件掩完,沒法寫啊。參考了別人的代碼硼砰,發(fā)現(xiàn)把循環(huán)該成遞歸且蓬,能很好的解決這個(gè)問題(針對這道題目只有[a-z], .的情況會產(chǎn)生回溯):

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 ) {
      // 這里通過邏輯或和遞歸题翰,把回溯的各個(gè)條件依次執(zhí)行
      return isMatchImpl( s, patterns.slice(1) ) || isMatchImpl( s.substr(1), patterns ) || isMatchImpl( s.substr(1), patterns.slice(1) );
    } else {
      // 這里通過邏輯或和遞歸恶阴,把回溯的各個(gè)條件依次執(zhí)行
      return isMatchImpl( s, patterns.slice(1) )
    }
  } else if (
    p.type==='.*'
  ) {
    if ( s ) {
      // 這里通過邏輯或和遞歸,把回溯的各個(gè)條件依次執(zhí)行
      return isMatchImpl( s, patterns.slice(1) ) || isMatchImpl( s.substr(1), patterns ) || isMatchImpl(s.substr(1), patterns.slice(1));
    } else {
      // 這里通過邏輯或和遞歸豹障,把回溯的各個(gè)條件依次執(zhí)行
      return isMatchImpl( s, patterns.slice(1) );
    }
  } else {

    return false;
  }
}

看下代碼里面的注釋冯事,通過邏輯或的邏輯短路原則,結(jié)合遞歸血公,就把回溯的各個(gè)路徑寫成一行了昵仅。循環(huán)和遞歸真是好基友,各有各的適用場景累魔。代碼的功能完成了摔笤,通過了官方的測試用例。

動態(tài)規(guī)劃

代碼完成了薛夜,但是執(zhí)行效率很有問題籍茧。看下上面講回溯例子時(shí)候的圖片梯澜,當(dāng)回溯的時(shí)候如果 subpattern 沒有變寞冯,且 strindex 沒有變渴析,那么結(jié)果是一致的,也就是說如果多次執(zhí)行可以被記錄下來吮龄,不用每次都判斷 subPatterns[strIndex] 是否匹配俭茧。這個(gè)思路和優(yōu)化斐波那契數(shù)列是否有點(diǎn)相似,就是對計(jì)算過的結(jié)果用空間換時(shí)間漓帚,對于相同的計(jì)算條件只需要計(jì)算一次母债。這個(gè)思路再加上這道題目,背后的原理其實(shí)就是動態(tài)規(guī)劃的概念尝抖。

我簡單說下什么叫動態(tài)規(guī)劃:

  1. 將待求解的問題分解為若干個(gè)子問題毡们。
  2. 按順序求解子階段,前一子問題的解昧辽,為后一子問題的求解提供了有用的信息衙熔。
  3. 在求解任一子問題時(shí),列出各種可能的局部解搅荞,通過決策保留那些有可能達(dá)到最優(yōu)的局部解红氯,丟棄其他局部解。

這個(gè)好像和我們正則匹配的過程不謀而合了:

  1. 匹配的過程是必須每個(gè) subpattern 都匹配成功咕痛。
  2. 下一個(gè) subpattern 所要匹配的字符取決于上一個(gè) subpattern 用到哪個(gè)字符痢甘。
  3. 當(dāng)一條匹配路徑走不通,通過回溯尋找另一條可能的匹配路徑茉贡。

而動態(tài)規(guī)劃在算法中的應(yīng)用塞栅,其一大優(yōu)化策略就是充分利用前面保存的子問題的解來減少重復(fù)計(jì)算。所以改造下代碼:

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;
  }

}

參考文獻(xiàn)

  1. https://zhuanlan.zhihu.com/p/27417442
  2. http://www.cnblogs.com/steven_oyj/archive/2010/05/22/1741374.html
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末块仆,一起剝皮案震驚了整個(gè)濱河市构蹬,隨后出現(xiàn)的幾起案子王暗,更是在濱河造成了極大的恐慌悔据,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,941評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件俗壹,死亡現(xiàn)場離奇詭異科汗,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)绷雏,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,397評論 3 395
  • 文/潘曉璐 我一進(jìn)店門头滔,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人涎显,你說我怎么就攤上這事坤检。” “怎么了期吓?”我有些...
    開封第一講書人閱讀 165,345評論 0 356
  • 文/不壞的土叔 我叫張陵早歇,是天一觀的道長。 經(jīng)常有香客問我,道長箭跳,這世上最難降的妖魔是什么晨另? 我笑而不...
    開封第一講書人閱讀 58,851評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮谱姓,結(jié)果婚禮上借尿,老公的妹妹穿的比我還像新娘。我一直安慰自己屉来,他們只是感情好路翻,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,868評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著茄靠,像睡著了一般帚桩。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上嘹黔,一...
    開封第一講書人閱讀 51,688評論 1 305
  • 那天账嚎,我揣著相機(jī)與錄音,去河邊找鬼儡蔓。 笑死郭蕉,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的喂江。 我是一名探鬼主播召锈,決...
    沈念sama閱讀 40,414評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼获询!你這毒婦竟也來了涨岁?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,319評論 0 276
  • 序言:老撾萬榮一對情侶失蹤吉嚣,失蹤者是張志新(化名)和其女友劉穎梢薪,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體尝哆,經(jīng)...
    沈念sama閱讀 45,775評論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡秉撇,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,945評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了秋泄。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片琐馆。...
    茶點(diǎn)故事閱讀 40,096評論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖恒序,靈堂內(nèi)的尸體忽然破棺而出瘦麸,到底是詐尸還是另有隱情,我是刑警寧澤歧胁,帶...
    沈念sama閱讀 35,789評論 5 346
  • 正文 年R本政府宣布滋饲,位于F島的核電站彤敛,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏了赌。R本人自食惡果不足惜墨榄,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,437評論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望勿她。 院中可真熱鬧袄秩,春花似錦、人聲如沸逢并。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,993評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽砍聊。三九已至背稼,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間玻蝌,已是汗流浹背蟹肘。 一陣腳步聲響...
    開封第一講書人閱讀 33,107評論 1 271
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留俯树,地道東北人帘腹。 一個(gè)月前我還...
    沈念sama閱讀 48,308評論 3 372
  • 正文 我出身青樓,卻偏偏與公主長得像许饿,于是被迫代替她去往敵國和親阳欲。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,037評論 2 355

推薦閱讀更多精彩內(nèi)容