字串匹配演算法之 BF 和 KMP 講解

梓航發表於2019-11-23

字串匹配演算法有很多種,現在為大家帶來 BF 和 KMP 的講解,其中講 BF 是為了與 KMP 演算法做一個對比,讓本文有由淺到深這樣的一個遞進關係。

BF 演算法

查詢一個字串是否在另一個字串中,我們最容易想到的方法就是使用兩個迴圈來解決,這種演算法就叫做 BF,全稱是 Brute-Force,中文翻譯為:暴力匹配。

這是 BF 匹配的過程

字串匹配演算法之 BF 和 KMP 講解

BF 演算法的思路

首先設文字串為 S,模式串為 P,假設文字串匹配到 i 位置,模式串匹配到 j 位置。

1、如果當前字元匹配成功(即 S[i] == P[j]),則 i++,j++,繼續匹配下一個字元;

2、如果失配(即 S[i] != P[j]),這時令 i = i - (j - 1),j = 0。

因為 i 的位置根據上述公式計算後,往前回退了,所以叫做 i 回溯。

至於 i = i - (j - 1) 這個公式是如何推匯出來的,可以這麼理解,你往前走了 j 步,要退回到原位,那就往回走 j 步就可以了,如果是要在原來的位置上再進一步,那就往回走 j - 1 步就可以了。

當然你也可以寫成 i = i - j + 1 表示回到原點後再往前走一步,更容易理解。

3、匹配成功後,返回 i - j,可以理解為往前走那麼多步後再回到原點。

BF 演算法程式碼實現

這裡我是用 JavaScript 實現的:

// for 迴圈的寫法
let str = "abdfadfadfgrereabdgfa";
let pattern = "dfadfg";

function BF(S, P) {
  for (let i = 0; i < S.length;) {
    for (let j = 0; j < P.length;) {
      if (S[i] === P[j]) {
        i++;
        j++;
      } else {
        i = i - (j - 1);
        break;
      }
      if (j === P.length) {
        return i - j;
      }
    }
  }
  return -1;
}

console.log(BF(str, pattern));
複製程式碼
// while 迴圈的兩種寫法
let str = "ababcdaaabbc ababc";
let pattern = "cda";

function BF(S, P) {
  let i = 0; let j = 0;
  while(i < S.length) {
    if (S[i] === P[j]) {
      i++;
      j++;
    } else {
      i = i - (j - 1);
      j = 0;
    }
    if (j === P.length) {
      return i - j;
    }
  }
  return -1;
}

console.log(BF(str, pattern));

function BF2(S, P) {
  let i = 0; let j = 0;
  while(i < S.length && j < P.length) {
    if (S[i] === P[j]) {
      i++;
      j++;
    } else {
      i = i - (j - 1);
      j = 0;
    }
  }
  if (j === P.length) {
    return i - j;
  }
  return -1;
}

console.log(BF2(str, pattern));
複製程式碼

KMP 演算法

接下來為大家介紹 KMP 演算法,KMP 的全稱是 Knuth-Morris-Pratt , 中文名:克努斯-莫里斯-普拉特,這個演算法是由 D.E.Knuth 和 V.R.Pratt 在 1974 年構思,同年 J.H.Morris 也獨立地設計出該演算法,最終由三人於 1977 年聯合發表。屬於字串匹配演算法中比較重要的一個演算法。

Donald Knuth

Donald Knuth

BF 和 KMP 的區別

我們來看一張圖對比一下 BF 和 KMP 的區別:

字串匹配演算法之 BF 和 KMP 講解

可以看到在失配時, BF 的 i 回溯的位置會很遠,同時 j 也會回溯到 0 位置;

而 KMP 的做法是 i 保持不變,只需要按條件設定好 j 的位置後,繼續往下匹配就可以了。

BF 的複雜度為 O(n*m),KMP 的複雜度為 O(n+m),所以 KMP 是一種比較高效的字串匹配演算法。

通過這張對比圖,我們知道,KMP 高效的原因就在於,只需要移動模式串就可以了。

那麼如何找到一種辦法來達到這個目的呢?

PMT 陣列的作用

KMP 提出了一個叫 PMT 的陣列來達到這個目的,它的全稱是 Partial-Match-Table,中文翻譯為:部分匹配表。

這個表是根據模式串生成出來的,裡面記錄的就是當失配後,模式串需要移動的位置。

我們看一下這個 PMT 長什麼樣:

字串匹配演算法之 BF 和 KMP 講解

可以看到模式串中的每一個字元都對應了一個數字,這個數字就是這個字元的 PMT 值,前面也說過這個 PMT 值就代表了當失配後,模式串需要移動的位置。

如何計算 PMT

那麼如何計算出 PMT 呢?答案是通過計算字串的前字尾來得出:

字串匹配演算法之 BF 和 KMP 講解

第一步,我們需要找到模式串的所有子串,需要按從前往後的順序來寫出所有子串(想象成一個集合,所以也會包括自己本身)。

第二步,找到每一個子串的前字尾組合,字首是不包含最後一個字元的子串集合,字尾是不包含第一個字元的子串集合,然後找到前字尾集合的交集,最後找到交集中字串長度最長的那個,這個字串的長度,就是我們需要的 PMT 值。

通過上圖可以看到,其真實模式串中每個字元對應的 PMT 值,就是這個子串的前字尾共有集合中,字串長度最大的值。

最大前字尾的小練習

接下來做一些找共有最大前字尾的小練習吧:

字串匹配演算法之 BF 和 KMP 講解

從這些練習中能發現什麼規律呢?沒錯,模式串的最大前字尾分別只出現在開頭和結尾,而且它們都是相等的。

那,這又代表了什麼意思呢?

字串匹配演算法之 BF 和 KMP 講解

到此為止,你應該能明白 PMT 的真正作用了吧!

KMP 的匹配過程

我們來看一遍 KMP 的匹配過程:

字串匹配演算法之 BF 和 KMP 講解

解釋一下圖中標記的三個點:

1、圖中 v 代表 PMT。

字串匹配演算法之 BF 和 KMP 講解

2、失配時,找 j - 1 位的 PMT 值,圖中是 2,即此時 PMT[j - 1] = 2。圖中標紅的字元,就是已經匹配過的子串 dfadf 中的前字尾。

字串匹配演算法之 BF 和 KMP 講解

3、接下來保持 i 不變,把 j 的值設為 PMT[j-1],也就是 2,這步操作就是把模式串的字首對齊了已匹配串的字尾,這就是所說的充分利用已知資訊來提高匹配效率。

PMT 陣列用途總結

共有最大前字尾分別處於原字串中的開始和結尾處,我們可以利用這個性質來記錄已知資訊,也就是已匹配過的字元,如果有大於 0 的 PMT 值,說明已匹配過的地方有需要再匹配避免遺漏的,否則都是 0 的話,j 就簡單地回溯到 0 就可以了,這就是 PMT 陣列的用途。

生成 PMT 的思路

知道 PMT 的用途後,那我們如何使用程式來生成 PMT 呢?

思路:

1、

字串匹配演算法之 BF 和 KMP 講解

2、

字串匹配演算法之 BF 和 KMP 講解

3、

字串匹配演算法之 BF 和 KMP 講解

4、

字串匹配演算法之 BF 和 KMP 講解

5、從匹配進入到失配狀態時 i 的回溯稍微有點複雜:

字串匹配演算法之 BF 和 KMP 講解

6、這時因為 i = 0 了,就可以進行比較了,如果 i != 0 那麼還要繼續重複第 5 和第 6 的過程。

字串匹配演算法之 BF 和 KMP 講解

程式計算的大致過程就是這樣的,你可以再做幾個小練習來鞏固一下:

字串匹配演算法之 BF 和 KMP 講解

KMP 演算法的程式碼實現

下面就是根據以上思路用程式來實現演算法了:

let str = "abdfadfadfgrereabdgfa";
let pattern = "dfadfg";

// 求 PMT
function partialMatchTable(P) {
  // 初始條件
  let PMT = [], i = 0, j = 1;
  PMT[0] = 0;
  while(j < P.length) {
    if (P[i] === P[j]) {
      i++;
      PMT[j] = i;
      j++;
    } else {
      if (i !== 0) {
        i = PMT[i - 1];
      } else {
        PMT[j] = 0;
        j++;
      }
    }
  }
  return PMT;
}

// KMP 主體
function KMP(S, P) {
  let i = 0, j = 0, TMP = partialMatchTable(P);
  while (i < S.length && j < P.length) {
    if (S[i] === P[j]) {
      i++;
      j++;
    } else {
      if (j !== 0) {
        j = TMP[j - 1];
      } else {
        i++;
      }
    }
  }
  if (j === P.length) {
    return i - j;
  }
  return -1;
}
console.log(KMP(str, pattern));
複製程式碼

本文在寫作過程中參考了很多優秀的文章和視訊,在此對他們的幫助表示感謝。

如果你在看本文時發現有不懂的地方或者是有不正確的地方,歡迎評論告訴我,我會繼續修訂。

參考資料

  1. www.bilibili.com/video/av324…
  2. www.cnblogs.com/rubylouvre/…
  3. www.zhihu.com/question/21…
  4. blog.csdn.net/v_july_v/ar…
  5. github.com/mission-pea…
  6. www.ruanyifeng.com/blog/2013/0…

相關文章