KMP算法

算法介紹

  • KMP算法是一種改進的字符串匹配算法,由D.E.Kunth,J.H.Morris和V.R.Pratt提出,KMP算法的功能是在一個主文本字符串s中查找模式串t出現的位置。
  • 在KMP算法中,對於每個模式串會先計算出模式串的內部匹配信息(即next數組),在匹配失敗時主串不回溯,模式串向右移動,避免從新檢查先前匹配的字符,加速了主串和模式串的匹配過程。

算法說明

設主串(下文中咱們稱做s)爲:"a b a c a a b a c a b a b b"
模式串(下文中咱們稱爲w)爲:"a b a c a b"
用暴力匹配字符串的過程當中,咱們會把s[0]和t[0]匹配,若是相同則匹配下一個字符,直到出現不相同的狀況,此時咱們會丟棄前面的匹配信息,而後把s[1] 跟 t[0]匹配,循環進行,直到主串結束,或者出現匹配成功的狀況。這種丟棄前面的匹配信息的方法,極大地下降了匹配效率。以下圖所示:
圖片.png
而KMP算法在匹配失敗時,主串不回溯,即i值保持不變,模式串向右移動j - next[j] (next數組的求法和爲啥這樣移動後面會詳細介紹,這裏能夠先混個眼熟~),j變成了next[j],下一次匹配時,s[i]和t[j]繼續進行比較,直到匹配成功,具體過程以下圖所示:
圖片.png

next數組(重點,敲黑板!!!)

  • next數組的計算
    以t="aaabc"爲例,咱們先來看next[3]的含義,由t.substr(0,3)獲得 的串tmp="aaa",next[3]表示tmp中最長相同的前綴和後綴的長度。tmp的前綴有"a"和"aa"兩種,tmp的後綴也有"a"和"aa"兩種,須要特別注意的是tmp串的前綴和後綴不能取tmp自己!顯然,tmp的前綴和後綴相同的有兩個,分別是"a"和"aa",但最長的是"aa",長度爲2。所以,就本例而言next[3] = 2。
    默認規定全部的next[0] = -1, 針對本例t="aaabc"而言,按照上面的分析方法,很容易得出next[1] = 0, next[2] = 1, next[3] = 2, next[4] = 0
  • next數組的含義
    next數組實際上保存的是模式串自身內部匹配的信息。

KMP匹配過程

仍是以圖2中的s串和t串爲例,當KMP第一次匹配失敗時,i不變,j變成了next[j],而後第二次匹配時,s[i]繼續和t[j]進行匹配,從圖2能夠看出,KMP第二次匹配時,是直接從t[1]開始匹配的,那如何保證t[1]以前的串和s串中的對應元素相同呢?具體問題以下圖所示:
圖片.png
顯然,這裏用到了模式串t自身的內部匹配信息,t的next數組以下:
圖片.png
當KMP第一次匹配失敗時,i = 5, j = 5, 此時 根據t的自身內部匹配信息和與s的局部匹配信息將t的子串的前綴移動到後綴的位置,移動距離以下圖所示,而後下一次匹配繼續從s[i]和t[j]開始:
圖片.png

代碼實現

1.next數組的求法html

vector<int> getNext(string t) {
// j表示當前位置的next值,具體含義是當前子串前綴和後綴相同的最大位數,初始化爲-1
    int i = 0, j = -1, n = t.size();
    vector<int> next(n);
    next[0] = -1;
    while(i < n) {
    //當j = -1時表示當前子串沒有相等的前綴和後綴
    //當t[i] = t[j]表示當前子串前綴的最後一位與後綴的最後一位相等,
    //此時,下一個next值爲爲當前next值加1(相似於dp的遞推關係)
        if(j == -1 || t[i] == t[j]) {
            i++;
            j++;
            //
            next[i] = j;
        }
        // 若是匹配不成功,須要將已經匹配的前綴長度進行回退,
        // 直到直到適合的匹配長度
        else
            j = next[j];
    }
    return next;
}

2.KMP匹配過程算法

int KMP(string s, string t) {
    if(s.empty() || s.size() < t.size()) return -1;
    int i = 0, j = 0, m = s.size(), n = t.size();
    // 求t的next數組
    vector<int> next = getNext(t);
    while(i < m && j < n) {
        // 當j = -1說明j已經回退到模式串的起始位置,沒法再次向前回退
        // 此時,須要將i移動到下一個位置,繼續將s[i]和t[0]進行匹配
        // 當s[i] = t[j]時,當前位匹配成功,兩個指針同時向後移動
        if(j == -1 || s[i] == t[j]) {
            i++;
            j++;
        }
        // 當兩個字符不相等時,依據模式串的next數組,將指針j回退到next[j]
        // 至關於將模式串t右移j-next[j],而後繼續將s[i]和t[j]進行匹配
        else
            j = next[j];
    }
    // 匹配成功,起始位置位i-j,不然返回-1,表示匹配失敗
    return j == n ? i - j : -1;
}

KMP算法的改進

KMP算法的改進是由朱洪大佬完成的,他主要改進了next數組的求法,讓主串和模式串匹配失敗時,模式串移動更快。咱們再來觀察圖2,當KMP第一次匹配失敗時,s[i] = 'a', t[j] = 'b',模式串t向右移動後,第二次匹配時再比較s[i]和t[j], 驚人地發現!!!移動後的t[j]仍然是'b',即t[j] = t[next[j]],顯然,因爲以前t[j]和s[i]已經匹配失敗,再換一個和t[j]相同的t[next[j]]再比較,確定失敗,這就至關於多了一次毫無心義的比較。所以,再計算next值以前,先判斷t[j] = t[next[j]]是否成立,若是成立,回退next值,讓next[i] = next[j],此時再向右移動模式串時,就能夠直接跳過這一次毫無心義的比較,代碼以下:
vector<int> getNextVal(string t) {
    int i = 0, j = -1, n = t.size();
    // 通常習慣把改進後的next數組稱爲nextVal
    vector<int> nextVal(n);
    next[0] = -1;
    while (i < n) {
        if (j == -1 || t[i] == t[j]) {
            i++;
            j++;
            // 若是下一個元素和它的next值所在的元素相等,回退它的next值
            if (t[i] == t[j])
                next[i] = next[j];
            else
                next[i] = j;
        }
        else
            j = next[j];
    }
    return next;
}
  • 改進後的next數組
    圖片.png
    注:紅色表示和以前next數組相比發生了變化
  • 改進後的KMP匹配:由以前的三次匹配變成了兩次匹配,以下圖所示:

圖片.png

KMP算法的時間複雜度分析

咱們用攤還分析來看KMP算法:
關於匹配指針的位置cur
操做A: 匹配時,cur++;
操做B: 失配時, cur = next[cur],這個next[cur] <= cur是成立的。
根據勢能分析(cur >= 0恆成立),咱們能夠證實,操做A的次數必定比操做B的次數要多,兩個操做都是O(1)。而操做A的執行次數很容易分析最壞上界是O(n)。那麼O(n) = T(A) >= T(B), 所以匹配的時間複雜度T(A+B) = O(n)。
對攤還分析還有疑問的看參考 https://www.cnblogs.com/elpsy...

旅程到此就圓滿結束了~~~

我是lioney,年輕的後端攻城獅一枚,愛鑽研,愛技術,愛分享。
我的筆記,整理不易,感謝閱讀、點贊和收藏。
文章有任何問題歡迎你們指出,也歡迎你們一塊兒交流後端各類問題!
相關文章
相關標籤/搜索