1. 程式人生 > 實用技巧 >字串KMP演算法

字串KMP演算法

一、演算法介紹:

KMP演算法主要用於字串包含問題,如:

給定字串A,B,判斷B是否是A的子串

在這裡,我們把等待匹配的字串A稱為母串,用來匹配的串B稱為模式串

二、演算法流程:

(感性理解???)


如果按照一般思路,我們迴圈A串的各個元素,判斷是否包含B串,演算法時間複雜度過高,因為這個演算法忽略了之前我們已經對A,B串進行了比較的事實,所以在執行時進行了許多重複的判斷,導致了時間複雜度過高。

因此,我們可以考慮對其進行優化~~

不要噴,我的圖是搜的,我真心不會畫圖qwq

首先,來看一個例子。我們設A串(上面的串)的陣列下標為A【1~n】,B串(下面的串)的陣列下標B【1~m】

經過比較我們可以發現,當 i=4, j=4 時,字串中的元素不一樣了,也就是說我們現在找到的子串不合法了,所以為了節省時間,我們要跳過儘可能多的元素(因為經過比較我們發現A串中1~3個元素都與B串中第一個元素不一樣了,所以不需要再浪費時間進行比較了)但是在跳過元素時也應當滿足當有重複元素時,保留重複元素的特性,所以我們應該讓B[1]與A[5]衝齊。

我們再看另一個例子

原來的字串是這樣的:

A B C A B C D H I J K

A B C E

我們發現當元素A E不一樣了,但是B串中的第一個元素A又可以和A串中的第四個元素A重合,所以我們就把字串B移動到當前位置

再來一個例子!

我們發現此時元素C D 不一樣,但是A串中的第三個元素A與B串中的第一個元素A是匹配的啊,所以,我們開始移動B串:

下一個例子也是一樣滴:

這個例子不進行過多解釋,還請讀者自行思考(但是我相信 聰明的你們一定都會!!)

(好吧這個例子好像挺重要的所以我還是講一講把TAT)

匹配之後我們發現元素C B不一樣,但是我們不可以直接把B中的第一個元素A挪到第i+1個位置,因為那樣做的話我們會失去許多重複元素(在這個題裡是A[4~5]和B[1~2],都是A B)。

所以正確的做法是當存在重複元素時,讓模式串與母串重合

從上面幾個例子中我們可以找到一些規律:

當匹配失敗後,j要移動的下一個位置K(在母串中的對應位置K哈,不是模式串),有著這樣的關係:


最前面的K個字元和J之前的K個字元是一樣的!!!

用數學公式表達就是:

P[0 ~ k-1] == P[j-k ~ j-1]

這樣的話,如果 我們首先設定 j 要移動下一個位置為p[j],那麼我們就可以在母串中尋找模式串,程式碼如下:

j=0;
for(int i=1;i<=n;i++){
    while(j>0&&a[i]!=b[j+1]) j=p[j];//如果不相等,那麼我們就回到重合部分的
                                //最後一個元素,從最後一個元素開始尋找 
    if(a[i]==b[j+1]) j++;
    if(j==0){
        cout<<i-m+1<<endl;//表示輸出當前相同字串的首字母下標 
        j=p[j];//因為可能存在多個包含關係,所以我們繼續搜尋 
    }
}

這樣,我們就完成了在母串中對於模式串的查詢。

But but,我們是不是忘記了什麼重要的東西???

我們還沒有說明怎麼找到陣列P啊!!!

重頭戲來了(手動滑稽)

我們已經知道,P陣列是用來尋找A B字串重合元素的最後一個位置的陣列變數(或許也可以稱之為指標??)那麼其實....我們可以直接把P陣列定義為:求模式串中的子串的真重複字首和字尾(強烈建議停下來好好理解一下這句話)

我們來看一個例子:

我們通過A B串的對比可以發現,其實在不匹配元素之前,我們找到的A B子串是等價的,所以我們完全可以把找A B 串的重合部分(其實不太準確)轉化為求B 串的字首字尾(可愛)

我相信聰明的您們都已經明白了

所以,我們就相當於在一個字串中再次運用KMP演算法(其實整個KMP演算法就是一個巢狀問題)

我自己造一個例子吧 唉~~

1 2 3 4 5 6 7 8 9

B a b c a b d e a c

P 0 0 0 1 2 0 0 0 ?

就這個吧

P陣列代表的是我們可以返回的元素的下標,(也可以理解為如果下一個元素不匹配,我們可以從當前元素找到一條“退路”),所以我們從頭開始推P陣列

因為B[1]=a,因為它是最左邊的元素,所以不可能再有退路,所以它的返回值就是0(相當於返回到陣列B[0])

因為B[2]=b,與B[1]=a不相等,所以如果我們發現元素不相等,我們應當再次返回B[0];

B[3]=c同理?

終於到了B[4]=a!!!(精神一震!!) 我們突然發現B[4]=a與B[1]=a相等,所以我們可以從B[4]跳到B[1];

B[5]=B[4+1]正好與B[2]=B[2+1]相等,也就是當前子串的字首和字尾可以匹配,所以我們直接從B[5]返回B[2];(還記得我們的P陣列返回的是重合元素的末尾地址嗎?)

其它同理。各位大大可以自己多推幾遍(掩蓋住我其實並不想多寫的事實)

好了!大體思路就是這樣,程式碼實現如下

int j=0;
for(int i=2;i<=m;i++)//因為我們的i表示當前元素是否與j+1匹配
//所以我們從i=2開始迴圈
{ while(j>0&&b[i]!=b[j+1]) j=p[j]; if(b[j+1]===b[i]) j++; p[i]=j; }//預處理模式串中所有的情況

總的來說,KMP和上面的程式碼其實還是很好理解的??(哼,才不告訴你我學了6h+呢)

其實各位大大們只要多看幾遍題解+自己推幾遍就可以理解了~~~~吧?

完整程式碼如下:

題目:洛谷P3375 【模板】KMP字串匹配https://www.luogu.com.cn/problem/P3375

#include<iostream>
#include<cstdio>
#include<bits/stdc++.h>
using namespace std;

int p[1020000],n,m,j;
char a[1020000],b[1020000];

int main() {
    cin>>a+1;
    cin>>b+1;
    n=strlen(a+1);
    m=strlen(b+1);
//    int j=0;
    for(int i=2; i<=m; i++) {
        while(j&&b[i]!=b[j+1]) j=p[j];
        if(b[j+1]==b[i]) j++;
        p[i]=j;
    }//預處理

    j=0;

    for(int i=1; i<=n; i++) {
        while(j>0&&a[i]!=b[j+1]) j=p[j];
        if(a[i]==b[j+1]) j++;
        if(j==m) {
//            cout<<" *";
            cout<<i-m+1<<endl;
            j=p[j];//再次跳回原來的地方,繼續判斷
        }
    }

    for(int i=1; i<=m; i++)
        cout<<p[i]<<" ";
    return 0;
}

——END——