字符串匹配基礎下——KMP 算法

在全部的字符串匹配算法中,KMP 算法是最知名的,實際上,它和 BM 算法的本質是同樣的。算法

1. KMP 算法基本原理

KMP 算法是根據三位做者(D.E.Knuth,J.H.Morris 和 V.R.Pratt)的名字來命名的,算法的全稱是 Knuth Morris Pratt 算法,簡稱爲 KMP 算法。數組

KMP 算法的核心思想和 BM 算法很是相近,就是在遇到不可匹配字符的時候,咱們但願能將模式串向後多滑動幾位,跳過那些確定不會匹配的狀況。數據結構

在 KMP 算法中,咱們從前向後對模式串和主串進行對比,不能匹配的字符仍然叫做壞字符,而前面已經匹配的字符串叫做好前綴框架

那遇到壞字符時,咱們怎麼決定模式串該向後移動幾位呢?函數

咱們須要在好前綴的全部後綴子串中,找到一個最長的能夠和好前綴的前綴子串匹配的子串。假設這個最長的可匹配的前綴子串爲 {v},長度爲 k,而壞字符在主串中對應的位置爲 i,在模式串中對應的位置爲 j,那麼咱們就須要將模式串後移 j-k 位,也就至關於把 j 更新爲 k(注意下圖的錯誤), i 不變,而後繼續比較。spa

咱們把好後綴的全部後綴子串中,最長的能夠和好前綴的前綴子串匹配的子串,叫做最長可匹配後綴子串,對應的前綴子串,叫做最長可匹配前綴子串.net

相似 BM 算法,咱們也能夠先對模式串進行預處理,定義一個數組來存儲模式串中每一個前綴(這些前綴都有多是好前綴)的最長可匹配前綴子串的結尾字符下標。咱們把這個數組定義爲 next 數組,不少書中還給這個數組起了一個名字,叫失效函數(failure function)。3d

數組的下標是每一個前綴結尾字符的下標,數組的值是這個前綴的最長能夠匹配的前綴子串的結尾字符下標。code

有了 next 數組,KMP 算法就很容易實現,下面咱們先給出一個程序的框架,假設 next 數組已經計算好了。cdn

int KMP(char str1[], int n, char str2[], int m) {
    int next[m];
    GenerateNext(str2, next, m);

    int j = 0;

    for (int i = 0; i < n; i++)
    {
        while (j > 0 && str1[i] != str2[j])
        {
            j = next[j-1] + 1; // j 更新爲最長可匹配前綴子串的長度 k
        }
        if (str1[i] == str2[j]) j++;
        if (j == m) return i - m + 1;
    }

    return -1;
}
複製代碼

2. 失效函數計算方法

咱們能夠用很是笨拙的方法來計算 next 數組。好比,若是要計算下面這個模式串 b 的 next[4],咱們就把 b[0, 4] 的全部後綴子串列舉出來,逐個看是否能和模式串的前綴子串匹配。這種方法雖然也能夠計算出 next 數組,可是效率很是低。

咱們按照下標從小到大,依次計算 next 數組的值。當咱們要計算 next[i] 的時候,前面的 next[0] 到 next[i-1] 都已經計算出來了,咱們能夠利用前面的值來快速推導出 next[i] 的值。

若是 next[i-1] = k-1,也就是說,子串 b[0, k-1] 是 b[0, i-1] 的最長可匹配前綴子串。若是子串 b[0, k-1] 的下一個字符 b[k],與 b[0, i-1] 的下一個字符 b[i] 匹配,那麼子串 b[0, k] 也就是 b[0, i] 的最長可匹配前綴子串。因此 next[i] = k。可是,若是子串 b[0, k-1] 的下一個字符 b[k] 與 b[0, i-1] 的下一個字符 b[i] 不匹配,那就不能簡單地經過 next[i-1] 來獲得 next[i] 了。

咱們假設 b[0, i] 的最長可匹配後綴子串是 b[r, i]。若是咱們把最後一個字符去掉,那 b[r, i-1] 確定是 b[0, i-1] 的可匹配後綴子串,但不必定是最長可匹配子串。

因此,既然 b[0, i-1] 最長可匹配後綴子串對應的模式串的前綴子串的下一個字符並不等於 b[i],那麼咱們就能夠考察 b[0, i-1] 的次長可匹配後綴子串 b[x, i-1] 對應的可匹配前綴子串 b[0, i-1-x] 的下一個字符 b[i-x] 是否等於 b[i]。若是相等,那麼 b[x, i] 就是 b[0, i] 的最長可匹配後綴子串。

但是,如何求得 b[0, i-1] 的次長可匹配後綴子串呢?次長可匹配後綴子串確定被包含在最長可匹配後綴子串中,而最長可匹配後綴子串又對應最長可匹配前綴子串 b[0, y]。因而,查找 b[0, i-1] 的次長可匹配後綴子串,這個問題就變成了,查找 b[0, y] 的最長可匹配後綴子串。

所以,咱們能夠考察全部的 b[0, i-1] 的可匹配後綴子串 b[y, i-1],直到找到一個可匹配的後綴子串,它對應的前綴子串的下一個字符等於 b[i],那這個 b[y, i] 就是 b[0, i] 的最長可匹配後綴子串。

void GenerateNext(char str[], int next[], int m) {
    next[0] = -1;
    int k = -1;

    for (int i = 1; i < m; i++)
    {
        while (k != -1 && str[k+1] != str[i])
        {
            k = next[k];
        }
        if (str[k+1] == str[i]) k++;
        next[i] = k;
    }
}
複製代碼

3. KMP 算法複雜度分析

空間複雜度很容易分析,KMP 算法只用到了一個額外的數組 next,其大小與模式串長度 m 相同,所以空間複雜度爲 O(m)。

KMP 算法包括兩部分,第一部分是構建 next 數組,第二部分是藉助 next 數組進行匹配。

先看第一部分,這部分代碼由兩個循環組成。咱們觀察變量 i 和 k 的值,i 從 1 增加到 m,而 k 並非每次在 for 循環裏都增長,k 的值不可能大於 m。在 while 循環裏, k = next[k],其值是在減少的,總的減少次數也確定小於 m。因此 next 數組計算的時間複雜度爲 O(m)。

再看第二部分,方法是相似的。i 從 0 增加到 n,而 j 並非每次在 for 循環裏都增長,j 的值不可能大於 n。在 while 循環裏, j = next[j-1] + 1,其值是在減少的,總的減少次數也確定小於 n。因此匹配的時間複雜度爲 O(n)。

綜上所述,KMP 算法總的時間複雜度爲 O(m+n)。

參考資料-極客時間專欄《數據結構與算法之美》

獲取更多精彩,請關注「seniusen」!

相關文章
相關標籤/搜索