KMP算法詳解

寫在前面:

歡迎轉載,轉載請在文章顯眼處註明出處:html

https://www.cnblogs.com/grcyh/p/10519791.html

起源

所謂KMP(看毛片233手動滑稽)算法,就是一種改進的字符串匹配算法,由D.E.Knuth,J.H.Morris和V.R.Pratt同時發現,所以人們稱它爲克努特——莫里斯——普拉特操做(簡稱KMP算法)。KMP算法的關鍵是利用匹配失敗後的信息,儘可能減小模式串與主串的匹配次數以達到快速匹配的目的。具體實現就是實現一個next()函數,函數自己包含了模式串的局部匹配信息。時間複雜度O(m+n)。
                             
——百度百科算法

前置知識:

  1. 輸入輸出數組

  2. 數組函數

  3. \(for\)循環spa

模式串匹配的概念:

模式串匹配,就是給定一個須要處理的文本串和一個須要在文本串中搜索的模式串,查詢在該文本串中(通常文本串應遠大於模式串),模式串的是否出現過,出現的次數和出現的位置等。指針

樸素算法:

首先要理解,樸素的單模式串匹配大概就是枚舉每個文本串元素,而後從這一位開始不斷向後比較,每次比較失敗以後都要從頭開始從新比對,那麼若是模式串和文本串是相似於這樣的:模式串\(aaaab\),文本串是\(aaabaaabaaab\),若是是這樣的話,咱們設模式串長度爲\(m\),文本串長度爲\(n\),那麼樸素的暴力算法就會被卡成\(O(nm)\),因而就有了那三個傢伙大佬的\(KMP\)(然而並不認識他們是誰233),下面咱們就要講\(KMP\)了,準備好!code

KMP:

在樸素算法中,咱們每次匹配失敗都不得不放棄以前全部的匹配進度,所以時間複雜度很高,而\(KMP\)算法的精髓就在於每次匹配失敗以後不會從模式串的開頭進行匹配,而是根據已知的匹配數據,跳回模式串一個特定的位置繼續進行匹配,並且對於模式串的每一位,都有一個惟一的「特定跳回位置」,從而節約時間。htm

好比咱們考慮一組樣例:blog

模式串:abcab
文本串:abcacababcab

首先,前四位按位匹配成功,遇到第五位不一樣,而這時,咱們選擇將模式串向右移三位,或者能夠理解爲移動到模式串中與失配字符相同的那一位。即咱們將兩個已經遍歷過的模式串字符重合,所以能夠不用一位一位地移動,而是根據相同的字符來實現快速移動。字符串

模式串:   abcab
文本串:abcacababcab

但有時不光只會有單個字符重複:

模式串:abcabc
文本串:abcabdababcabc

當咱們發如今第六位失配時,咱們能夠將模式串的第一二位移動到第四五位,由於它們相同\(emmmmmm\)

模式串:   abcabc
文本串:abcabdababcabc

那麼如今讀者應該多少有些明白了, KMP 節約時間就在於用一個特定的位置來肯定當某一位失配時,咱們能夠將前一位跳躍到以前匹配過的某一位。咱們把這個特定的位置叫作失配指針(固然本人習慣這麼叫,大家也能夠叫什麼失敗指針之類的,都無所謂)。

n個指針,即爲一個數組。

首先咱們的失配數組應當1創建在模式串意義下,而不是文本串意義下。由於模式串長度小於文本串,要更加靈活,在失配後換位時,能夠更加靈活的處理。

那麼怎麼找到這個位置呢?
在模式串s1中,對於第i個位置,它的失配指針應當指向一個位置j,知足:

  1. \(j \leq i\)
  2. \(s1[i]==s1[j]\)
  3. \(j!=1\)時理應知足\(s1[1]\)\(s1(j-1)\)分別與\(s(i-j+1)\)\(str1(i-1)\)按位相等

咱們也能夠從先後綴的角度來理解失配數組(先後綴是什麼就不說了吧,應該你們都知道):

這裏定義真前綴和真後綴:對於一個字符串\(s_i\)\(s_j\),任意的\(s_x\)\(s_j\)都是這個字符串的真前綴\((i < x \leq j)\),同理,真後綴你們就都懂了吧?

那麼,這裏位置\(i\)失配數組的值就是模式串\(s1_1\)\(s1_i\)的最大公共真前綴和真後綴的長度。。有點繞??\(emmmmm\)……舉個栗子(後面咱們把失配數組定義爲\(kmp\)數組)

以字符串\("abcdabd"\)爲例,
"a"的真前綴和真後綴都爲空集,共有元素的長度爲0,所以,kmp[1]=0;
"ab"的真前綴爲[a],真後綴爲[b],共有元素的長度爲0,所以,kmp[2]=0;
"abc"的真前綴爲[a, ab],真後綴爲[bc, c],共有元素的長度0,所以,kmp[3]=0;
"abcd"的真前綴爲[a, ab, abc],真後綴爲[bcd, cd, d],共有元素的長度爲0,所以,kmp[4]=0;
"abcda"的真前綴爲[a, ab, abc, abcd],真後綴爲[bcda, cda, da, a],共有元素爲"a",長度爲1,所以,kmp[5]=1;
"abcdab"的真前綴爲[a, ab, abc, abcd, abcda],真後綴爲[bcdab, cdab, dab, ab, b],共有元素爲"ab",長度爲2,所以,kmp[6]=2;
"abcdabd"的真前綴爲[a, ab, abc, abcd, abcda, abcdab],真後綴爲[bcdabd, cdabd, dabd, abd, bd, d],共有元素的長度爲0,所以,kmp[7]=0。

即咱們的操做只是針對模式串的前綴--−−畢竟是失配指針,失配以後只有多是某個部分前綴須要「快速移動」。

而後利用\(kmp\)數組進行字符串匹配便可(若是還不懂的話看代碼吧)。

代碼實現

在文本串中找模式串:

//其中k能夠看作表示當前已經匹配完的模式串的最後一位的位置,你也能夠理解爲表示模式串匹配到第幾位了 
k=0;            //把計數器清零。
for(int i=0;i<n;++i) {
    while(k&&s1[i]!=s2[k]) k=kmp[k];              
    //匹配失敗就沿着失配指針往回調,跳到模式串的第一位就不用再跳了。
    if(s1[i]==s2[k]) ++k;                  //匹配成功那麼匹配到的模式串位置+1。                                         
    if(k==m) printf("%d\n",i-m+2);         //找到一個模式串,輸出位置便可。
  }

求失配數組:

//即爲一個模式串自身匹配自身的過程,剛剛說過失配數組是創建在模式串的意義下的,跟與文本串匹配思路同樣
int n=strlen(s1),m=strlen(s2);
  for(int i=1;i<m;++i) {
    while(k&&s2[i]!=s2[k]) k=kmp[k];
    if(s2[i]==s2[k]) kmp[i+1]=++k;
  }

時間複雜度:

最壞時間複雜度爲O\((n+m)\)(從代碼上看挺顯然的吧)

模板題

洛谷的一道模板題:

https://www.luogu.org/problemnew/show/P3375

完整代碼:

#include<cstdio>
#include<cstring>
#define maxn 1000007
using namespace std;
int k,kmp[maxn];
char s1[maxn],s2[maxn];
int main() {
  scanf("%s%s",s1,s2);
  int n=strlen(s1),m=strlen(s2);
  for(int i=1;i<m;++i) {
    while(k&&s2[i]!=s2[k]) k=kmp[k];
    if(s2[i]==s2[k]) kmp[i+1]=++k;
  }
  k=0;
  for(int i=0;i<n;++i) {
    while(k&&s1[i]!=s2[k]) k=kmp[k];
    if(s1[i]==s2[k]) ++k;
    if(k==m) printf("%d\n",i-m+2);
  }
  for(int i=1;i<=m;++i) printf("%d ",kmp[i]);
  printf("\n");
  return 0;
}

後記:

\(KMP\)算法的精髓在於\(kmp\)(有些人也叫它\(next\)數組)數組,尤爲是那個關於先後綴的那塊思路很重要,還須要你們多作題,慢慢的感悟,擴展\(KMP\)尚未學(先留個坑),等之後學了再補。

相關文章
相關標籤/搜索