日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

字符串匹配之KMP(KnuthMorrisPratt)算法(图解)

發布時間:2023/12/13 编程问答 24 豆豆
生活随笔 收集整理的這篇文章主要介紹了 字符串匹配之KMP(KnuthMorrisPratt)算法(图解) 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

文章目錄

  • 最長相等前后綴
  • next數組
    • 概念
    • 代碼實現
    • 圖解GetNext中的回溯
    • 改進
    • 代碼實現
  • 代碼
  • 復雜度分析


最長相等前后綴

給出一個字符串 ababa

前綴集合:{a, ab, aba, abab}

后綴集合:{a, ba, aba, baba}

相等前后綴 即上面用同樣顏色標識出來的集合元素,最長相等前后綴 也就是所有 相等前后綴 中最長的那一個,也就是上面的 aba 。用圖片舉例:

最長相等前后綴 就是 KMP 算法滑動的依據。我們用 next 數組存儲 最長相等前后綴,以避免每次需要用到 最長相等前后綴 時都需要遍歷尋找的繁瑣。


next數組

概念

next[i]=j 的含義是:下標 i 之前的字符串其 最長相等前后綴 的長度為 j 。next[0]= -1 (前面沒有字符串單獨處理)。

ababacd
next[0] = -1next[1] = 0next[2] = 0next[3] = 1next[4] = 2next[5] = 3next[6] = 0


當 s1[5] != s2[5] 時,移動 s2,讓 s2 的前綴(ababa)匹配 s1 的后綴(ababa),即比較 s1[5] 和 s2[next[5]] 。移動的距離是 不匹配位置下標相等前綴 之間的字符數量,即 5-3=2 。

從上面的例子中可以看出,next 的作用有兩個:

  • 表示該處字符不匹配時應該回溯到的字符的下標。
  • 上文提到的:下標 i 之前的字符串其 最長相等前后綴 的長度。

  • 代碼實現

    class Solution {public:void GetNext(const string& s, vector<int>& next) {int i = 0, j = -1;next[0] = -1; // 下標為0的字符前沒有字符串while (i < next.size() - 1) { // 因為函數體中每次先對i++,再對next[i]進行賦值// 因此i需要小于next.size() - 1,以保證自增時不越界if (j == -1 || s[i] == s[j]) {i++;j++; /* 關于 j *//*s[i] == s[j]成立時,next[i] 在 next[i - 1] 的值(j)的基礎上 + 1換言之,也就意味著相等前后綴的長度+1,新后綴結尾 i+1 對應的前綴結尾為 j+1*//* j == -1成立時,說明不存在相等前后綴,因此 i 之前的字符串的相等前后綴長度為 next[i] = (-1)++ = 0 */ next[i] = j;}else {j = next[j];// next[j] 是回溯的位置,是 j 指向的字符 之前的字符串的最長相等前后綴的長度// 該操作為了將前綴移動到后綴的位置上,假設 相等長度為 m// 相當于將 (0, j-m)、(1, j-m+1)...(m-1, j-1)匹配上// 舉個例子:// 字符串:a b a b a c d// next: -1 0 0 1 2 3// j i// 由于 j 指向的 字符b 其之前的 字符串 aba 最長相等前后綴的長度為 1,// 下標1 作為 新j 就將(0, j-1)匹配上了// 換言之,只需要將 下標1 作為 新j 即可將求 ababac 最長相等前后綴問題轉換為// 求 abac 最長相等前后綴的問題。}}}void getNext(const string& pattern, vector<int>& next){ // 另一種寫法int i, j = 0;next[0] = -1; //第一個位置不存在數據,為-1for (int i = 1; i < next.size(); i++){//如果當前位置沒有匹配前綴,則回溯到求當前后綴的最長可匹配前綴while (j != 0 && pattern[j] != pattern[i]){j = next[j];}//如果該位置匹配,則在next數組在上一個位置的基礎上加一if (pattern[j] == pattern[i]){j++;}next[i] = j;}} };

    關于提到的另一種寫法,這里不多做分析,可以閱讀凌桓大佬的博客。


    圖解GetNext中的回溯

    舉個直觀的例子:

    • 紅色部分分別為:最長相等前、后綴。

    • 藍色部分為雙指針指向的,待匹配的元素。

    • 黑色部分為未開始匹配的部分。

    • 綠色部分為 next 數組。

    • 如果 s[i] == s[j] ,雙指針同時后移,紅色區域變大。

    • 如果不匹配,必須在 紅色部分 重新尋找 相等前后綴,新的相等前后綴長度必然縮短。

    紫色部分是紅色部分的最長相等前后綴,可以看到,四個紫色部分都是完全相等的。同時,改變 j 的指向,回溯后 j = next[j] :

  • 此時,若 s[i] == s[j],又因 j 前面的紫色部分和 i 前面的紫色部分完全相等。則最長相等前后綴長度+1。
  • 不等則進行下一次回溯。圖中下一次回溯時不再有相等前后綴,因此不再有紫色部分,不斷地回溯,直到 j 指向 -1,此時觸發判定條件,執行 j++; i++; next[i]=j; 。

  • 改進

    舉個例子:

    • 主串 s = “aaaabaaaaac”
    • 子串 t = “aaaac”

    當 b 與 c 不匹配時應該 b 與下標為 next[c] = 3 的元素 a 比,這顯然是不匹配的,繼續回溯,next[next[c]] 回溯后的字符依然是 a 。此時已經 沒有必要再將進行回溯了。

    節省效率的做法應該是當 b 和 next[3] 不匹配時,就直接回溯到首個 a 的 next 指向(即下標 -1)。即:

    下標01234
    元素aaaac
    next-10123
    fail-1-1-1-13

    規則:

    • 如果 i 位字符與它 next 值指向的 j 位字符相等,則該 i 位的 fail 就指向 j 位的 fail 值;
    • 如果不等,則該 i 位的 fail 值就是它自己的 next 值。

    舉個其他例子 ababaaab,進一步體會:

    下標01234567
    元素ababaaab
    next-10012311
    fail-10-10-1310

    代碼實現

    這里用 next 表示上面提到的 fail 數組。

    void getNext(const string& s, vector<int>& fail) {int i = 1, j = 0;fail[0] = -1; // 下標為0的字符前沒有字符串while (i < fail.size()) {if (s[i] != s[j]) { // 字符不匹配fail[i] = j; // 不等時,fail[i] = next[i] = jj = fail[j]; // 回溯}else { // 字符匹配fail[i] = fail[j]; // i 指向的字符與 j 指向字符相等}j++;i++;} }

    輸出結果:


    代碼

    class Solution { public:void GetNext(const string& s, vector<int>& next) {int i = 0, j = -1;next[0] = -1; // 下標為0的字符前沒有字符串while (i < next.size()-1) { if (j == -1 || s[i] == s[j]) {i++;j++;next[i] = j;}else {// 如果當前位置沒有匹配前綴,則回溯到求當前后綴的最長可匹配前綴j = next[j];}}}void getNext(const string& s, vector<int>& fail) { // 改進的next數組int i = 1, j = 0;fail[0] = -1; // 下標為0的字符前沒有字符串while (i < fail.size()) {if (s[i] != s[j]) { // 字符不匹配fail[i] = j; // 不等時,fail[i] = next[i] = jj = fail[j]; // 回溯}else { // 字符匹配fail[i] = fail[j]; // i 指向的字符與 j 指向字符相等}j++;i++;}}int knuthMorrisPratt(const string& query, const string& pattern) {//不滿足條件則直接返回falseif (query.empty() || pattern.empty() || query.size() < pattern.size()){return -1;}int i = 0, j = 0;int len1 = query.size(), len2 = pattern.size();vector<int> next(pattern.size(), -1); // next數組GetNext(pattern, next);while (i < len1 && j < len2){if (j == -1 || query[i] == pattern[j]){i++;j++; // i、j各增1}else j = next[j]; // i不變,j回溯}if (j == len2)return(i - len2); // 返回匹配模式串的首字符下標elsereturn -1; // 返回不匹配標志} };

    復雜度分析

    • 空間復雜度O(M): 需要借助到一個 next 數組,數組長度為 MMMMMM 為模式串長度。
    • 時間復雜度O(N + M): 時間復雜度主要包含兩個部分,next 數組的構建以及對主串的遍歷:next 數組構建的時間復雜度為 O(M);后面匹配中主串不回溯,循環時間復雜度為 O(N),所以 KMP 算法的時間復雜度為 O(N + M)。

    總結

    以上是生活随笔為你收集整理的字符串匹配之KMP(KnuthMorrisPratt)算法(图解)的全部內容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。