字符串匹配算法

BF算法

BF(Brute Force),暴力檢索法是最好想到的算法,也最好實(shí)現(xiàn)。首先將原字符串和子串左端對齊,逐一比較;如果第一個(gè)字符不能匹配,則子串向后移動(dòng)一位繼續(xù)比較;如果第一個(gè)字符匹配,則繼續(xù)比較后續(xù)字符,直至全部匹配。 時(shí)間復(fù)雜度:O(nm)。其中 n 為原字符串長度,m 為子串長度。

BF.jpg
function BF(haystack, needle) {       
    let j=0; 

    for(let i = 0; i < haystack.length; i++){
        if(haystack[i] == needle[j]){
            if(j == 0){
                re = i;
            }
            j = j+1;
            if(j == needle.length) {
                return i-j+1;
            }
        }else {
            if(j !== 0){
                i=i-j;
            }
            j=0;
        }
    }
    return -1;
}       

RK算法

RK算法在BF基礎(chǔ)上,引入哈希算法。通過字符串的哈希值的比較替換掉字符串之間的比較,從而降低算法的時(shí)間復(fù)雜度。RK算法整體的時(shí)間復(fù)雜度為O(n)。其中 n 為原字符串長度。

// 生成hash值
function hash(string) {
    let hash = 0;
    for (let i = 0; i < string.length; i++) {
        hash += 26 * hash + string[i].charCodeAt();
    }
    return hash;
}
// 比較兩個(gè)字符串是否相等
function isMatch (str, dest) {
    if (str.length !== dest.length) {
        return false;
    }
    for (var i = 0; i < str.length; i++) {
        if (str[i] !== dest[i]) {
            return false;
        }
    }
    return true;
}

function RK(haystack, needle) {
    let needleHash = hash(needle);

    for(let i=0; i<=(haystack.length - needle.length); i++){
        let subStr = haystack.substr(i, needle.length);
        if (hash(subStr) === needleHash && isMatch(subStr, needle)) {
            return i;
        }
    }

    return -1;
}       

BM算法

BM算法的核心思想是通過將模式串沿著主串大踏步的向后滑動(dòng),從而大大減少比較次數(shù),降低時(shí)間復(fù)雜度。而算法的關(guān)鍵在于如何兼顧步子邁得足夠大與無遺漏,同時(shí)要盡量提高執(zhí)行效率。這就需要模式串在向后滑動(dòng)時(shí),遵守壞字符規(guī)則與好后綴規(guī)則,同時(shí)采用一些技巧。

壞字符

壞字符規(guī)則:從后往前逐位比較模式串與主串的字符,當(dāng)找到不匹配的壞字符時(shí),記錄模式串的下標(biāo)值si,并找到壞字符在模式串中,位于下標(biāo)si前的最近位置xi(若無則記為-1),si-xi即為向后滑動(dòng)距離。(PS:我覺得加上xi必須在si前面,也就是比si小的條件,就不用擔(dān)心計(jì)算出的距離為負(fù)了)。但是壞字符規(guī)則向后滑動(dòng)的步幅還不夠大,于是需要好后綴規(guī)則。

好后綴

好后綴規(guī)則:從后往前逐位比較模式串與主串的字符,當(dāng)出現(xiàn)壞字符時(shí)停止。若存在已匹配成功的子串{u},那么在模式串的{u}前面找到最近的{u},記作{u'}。再將模式串后移,使得模式串的{u'}與主串的{u}重疊。若不存在{u'},則直接把模式串移到主串的{u}后面。為了沒有遺漏,需要找到最長的、能夠跟模式串的前綴子串匹配的,好后綴的后綴子串(同時(shí)也是模式串的后綴子串)。然后把模式串向右移到其左邊界,與這個(gè)好后綴的后綴子串在主串中的左邊界對齊。

何時(shí)使用壞字符規(guī)則和好后綴規(guī)則呢?首先在每次匹配過程中,一旦發(fā)現(xiàn)壞字符,先執(zhí)行壞字符規(guī)則,如果發(fā)現(xiàn)存在好后綴,還要執(zhí)行好后綴規(guī)則,并從兩者中選擇后移距離最大的方案執(zhí)行。

技巧

1.通過散列表實(shí)現(xiàn),壞字符在模式串中下標(biāo)位置的快速查詢。
2.每次執(zhí)行好后綴原則時(shí),都會計(jì)算多次能夠與模式串前綴子串相匹配的好后綴的最長后綴子串。為了提高效率,可以預(yù)先計(jì)算模式串的所有后綴子串,在模式串中與之匹配的另一個(gè)子串的位置。同時(shí)預(yù)計(jì)算模式串中(同長度的)后綴子串與前綴子串是否匹配并記錄。在具體操作中直接使用,大大提高效率。
3.如何快速記錄模式串后綴子串匹配的另一個(gè)子串位置,以及模式串(相同長度)前綴與后綴子串石否匹配呢?先用一個(gè)suffix數(shù)組,下標(biāo)值k為后綴子串的長度,從模式串下標(biāo)為i(0~m-2)的字符為最后一個(gè)字符,查找這個(gè)子串是否與后綴子串匹配,若匹配則將子串起始位置的下標(biāo)值j賦給suffix[k]。若j為0,說明這個(gè)匹配子串的起始位置為模式串的起始位置,則用一個(gè)數(shù)組prefix,將prefix[k]設(shè)為true,否則設(shè)為false。k從0到m(模式串的長度)于是就得到了模式串所有前綴與后綴子串的匹配情況。

實(shí)現(xiàn)

僅有壞字符規(guī)則:

// 本例只實(shí)現(xiàn)從'a'-'z'的字符串匹配
function hashMap(needle) {
  let hash = [];
  for(let i=0; i<26; i++){
      hash[i] = -1;
  }
  // 對于存在多個(gè)xi,則取靠后的那個(gè)下標(biāo),防止滑動(dòng)過多
  for(let i=0; i<needle.length; i++){
      let ascii = needle[i].charCodeAt() - 97;
      hash[ascii] = i;
  }
  return hash;
}

function BM(haystack, needle) {
  let n = haystack.length;
  let m = needle.length;

  let hash = hashMap(needle);

  let i = 0;
  while(i <= n-m) {
      let bad = -1;
      for(let j = m-1; j>=0; j--){
          if(haystack[i+j] !== needle[j]){
              bad = j;
              break;
          }
      };
      if(bad === -1){
          return i;
      }
      let ascii = haystack[bad+i].charCodeAt() - 97;
      i = i + (bad - hash[ascii]);
  }
  return -1;
}

壞字符規(guī)則在某些場景下會使si-xi為負(fù)值,導(dǎo)致無限循環(huán)。如在“aaaaaaa”中匹配"baaaa"。

下面講好后綴原則加入,完整代碼如下:

function suffixAndPrefix(needle) {
  let m = needle.length;
  let suffix = [];
  let prefix = [];
  for(let i=0; i<m; i++){
    suffix[i] = -1;
    prefix[i] = false;
  }

  for(let i=0; i<m-1; i++){
    let j=i;
    let k=0;
    while(j>=0 && needle[j] === needle[m-1-k]) {
      j--;
      k++;
      suffix[k] = j+1;
    }
    if(j===-1){
      prefix[k]=true;
    }
  }
  return [suffix, prefix];
}

function moveByGoodFix(j, m, suffix, prefix) {
  let k = m - 1 - j;
  if(suffix[k] !== -1) return j - suffix[k] + 1; // 如果存在匹配的好后綴子集,滑動(dòng)到壞字符的下一位
  // TODO 為什么要尋找?
  for(let i=j+2; i<=m-1; i++) {
    if(prefix[m-i] === true) {
      return i;
    }
  }
  return m;
}

function hashMap(needle) {
  let hash = [];
  for(let i=0; i<26; i++){
      hash[i] = -1;
  }
  // 對于存在多個(gè)xi,則取靠后的那個(gè)下標(biāo),防止滑動(dòng)過多
  for(let i=0; i<needle.length; i++){
      let ascii = needle[i].charCodeAt() - 97;
      hash[ascii] = i;
  }
  return hash;
}

function BM(haystack, needle) {
  let n = haystack.length;
  let m = needle.length;

  let [ suffix, prefix ] = suffixAndPrefix(needle);
  let hash = hashMap(needle);

  let i = 0;
  while(i <= n-m) {
      let bad = -1;
      for(let j = m-1; j>=0; j--){
          if(haystack[i+j] !== needle[j]){
              bad = j;
              break;
          }
      };
      if(bad === -1){
          return i;
      }
      let ascii = haystack[bad+i].charCodeAt() - 97;
      let badChar = bad - hash[ascii];
      let goodFix = 0;
      // 判斷是否有好后綴
      if(bad<m-1){
        goodFix = moveByGoodFix(bad, m, suffix, prefix);
      }

      i = i + Math.max(badChar, goodFix);
    }
  return -1;
}

KMP算法

PMT數(shù)組

KMP算法的核心,是一個(gè)被稱為部分匹配表(Partial Match Table)的數(shù)組。

PMT.jpg

PMT中的值是字符串的前綴集合與后綴集合的交集中最長元素的長度

next數(shù)組

為了編程的方便, 我們不直接使用PMT數(shù)組,而是將PMT數(shù)組向后偏移一位。我們把新得到的這個(gè)數(shù)組稱為next數(shù)組。

next.jpg
function next(needle) {
  let res = [];
  res[0] = -1;

  let k = -1;

  for (let i = 1; i < needle.length; i++) {
      while (k != -1 && needle[i] != needle[k+1]) {
          k = res[k]; // 當(dāng)不匹配的時(shí)候,回溯尋找次長串
      }
      if (needle[i] === needle[k+1]) {
          k++;
      }
      res[i] = k;
  }
  return res;
}

function KMP(haystack, needle) {
  let n = haystack.length;
  let m = needle.length;

  let nextArray = next(needle);

  let j = 0;

  for(let i=0; i<n; i++){
    while(j>0 && haystack[i] !== needle[j]){
      j = nextArray[j-1] + 1;
    }
    if(haystack[i] == needle[j]) {
      j++;
    }
    if(j == m){
      return i-m+1;
    }
  }

  return -1;
}

算法比較

名稱 空間復(fù)雜度 最好時(shí)間復(fù)雜度 最差時(shí)間復(fù)雜度
BF算法 T(1) O(nm) O(nm)
RK算法 T(1) O(n+m) O(nm)
BM算法 T(2m) O(n) O(nm)
KMP算法 T(m) O(n+m) O(nm)

學(xué)習(xí)心得

BF算法是最容易想到的算法,只需要逐個(gè)字符去比較,遇到不匹配的字符只需要將主串字符向后移動(dòng)一位,重復(fù)比較即可。

RK算法在BF的基礎(chǔ)上,引入了hash值。核心理念是:hash值不相同的兩個(gè)字符串一定不想等,hash相等的字符串才有可能相等。通過hash值的運(yùn)算大大降低了字符比較的次數(shù)。

BM算法提出壞字符和好后綴的規(guī)則,從字符串的尾部開始比較。遇到壞字符則大幅度向后滑動(dòng),好后綴規(guī)則是記錄模式串中前后是否有相同的部分。兩個(gè)規(guī)則中移動(dòng)距離比較遠(yuǎn)的,則成為下一次循環(huán)比較的開始。

KMP算法在BM算法的基礎(chǔ)上,直接先計(jì)算模式串的“重復(fù)度”即模式串的前后字符是否有相同的部分,匹配到不等的字符就可以把之前比較相等的部分跳過。

BM和KMP都是處理模式串本身,與主串無關(guān)。都是為了在下一次比較的時(shí)候能夠大幅度的向后移動(dòng),以提高字符串匹配的速度。

參考資料

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 單模式串匹配算法中BM(Boyer-Moore)算法算是很難理解的算法了,不過性能高效,據(jù)說比KMP算法性能提升3...
    堯字節(jié)閱讀 988評論 0 3
  • ??在文本處理中,關(guān)鍵字匹配是一個(gè)十分常用且重要的功能。關(guān)鍵字稱為模式串,在文本T中尋找模式串P出現(xiàn)的所有出現(xiàn)的位...
    老羊_肖恩閱讀 4,663評論 1 4
  • 以下為學(xué)習(xí) 《數(shù)據(jù)結(jié)構(gòu)與算法之美 -- 字符串匹配》 的記錄。 BF算法 即暴力匹配算法,循環(huán)遍歷匹配。 RK算法...
    微微笑的蝸牛閱讀 2,531評論 0 52
  • 一、簡介 此文介紹另一款更高效的字符串匹配算法,據(jù)稱較 KMP 算法而言,效率提高了3~5倍。 該算法由 Bob ...
    goldenJetty閱讀 823評論 1 4
  • 我叫小花,是一位母親。 我的孩子們上個(gè)月剛出生,一男一女,毛茸茸的,走路還有點(diǎn)趔趄。附近一起出去逛過林子的阿黃說,...
    普利斯東特先生閱讀 201評論 1 0

友情鏈接更多精彩內(nèi)容