KMP算法(研究總結,字符串)

KMP算法(研究總結,字符串)

前段時間學習KMP算法,感受有些複雜,不過好歹是弄懂啦,簡單地記錄一下,方便之後本身回憶。算法

引入

首先咱們來看一個例子,如今有兩個字符串A和B,問你在A中是否有B,有幾個?爲了方便敘述,咱們先給定兩個字符串的值
A="abcaabababaa"
B="abab"

那麼普通的匹配是怎麼操做的呢?
固然就是一位一位地比啦。(下面用藍色表示已經匹配,黑色表示匹配失敗)
此處輸入圖片的描述
可是咱們發現這樣匹配很浪費!
爲何這麼說呢,咱們看到第4步:
此處輸入圖片的描述
在第4步的時候,咱們發現第3位上c與a不匹配,而後第五步的時候咱們把B串向後移一位,再從第一個開始匹配。
此處輸入圖片的描述
這裏就有一個對已知信息很大的浪費,由於根據前面的匹配結果,咱們知道B串的前兩位是ab,因此無論怎麼移,都是不能和b匹配的,因此應該直接跳過對A串第二位的匹配,對於A串的第三位也是同理。數組

或許這這個例子還不夠經典,咱們再舉一個。

A="abbaabbbabaa"
B="abbaaba"
學習

在這個例子中,咱們依然從第1位開始匹配,直到匹配失敗:

abbaab
bbabba
abbaaba
咱們發現第7位不匹配
那麼咱們若按照原來的方式繼續匹配,則是把B串向後移一位,從新從第一個字符開始匹配
abbaabbbabba
_abbaaba
依然不匹配,那咱們就要繼續日後移咯。
且住!
既然咱們已經匹配了前面的6位,那麼咱們也就知道了A串這6位和B串的前6位是匹配的,咱們可否利用這個信息來優化咱們的匹配呢?
也就是說,咱們能不能在上面匹配失敗後直接跳到:
abbaabbbabba
____abbaaba
這樣就能夠省去不少沒必要要的匹配。優化

KMP算法

KMP算法就是解決上面的問題的,在講述以前,咱們先擺出兩個概念:3d

前綴:指的是字符串的子串中從原串最前面開始的子串,如abcdef的前綴有:a,ab,abc,abcd,abcde
後綴:指的是字符串的子串中在原串結尾處結尾的子串,如abcdef的後綴有:f,ef,def,cdef,bcdefcode

KMP算法引入了一個F數組(在不少文章中會稱爲next,但筆者更習慣用F,這更方便表達),F[i]表示的是前i的字符組成的這個子串最長的相同前綴後綴的長度!
怎麼理解呢?
例如字符串aababaaba的相同前綴後綴有a和aaba,那麼其中最長的就是aaba。blog

KMP算法的難理解之處與本文敘述的約定

在繼續咱們的講述以前,筆者首先講一下爲何KMP算法不是很好理解。
雖說網上關於KMP算法的博客、教程不少,但筆者查閱不少資料,詳細講述過程及原理的很少,真正講得好的文章在定義方面又有細微的不一樣(固然,真正寫得好的文章也有,這裏就不一一列舉),好比說有些從1開始標號,有些next表示的是前一個而有些是當前的,通讀下來,不免會混亂。
那麼,爲了防止讀者在接下來的內容中感到和筆者以前學習時一樣的困惑,在這裏先對下文作一些說明和約定。教程

1.本文中,全部的字符串從0開始編號
2.本文中,F數組(即其餘文章中的next),F[i]表示0~i的字符串的最長相同前綴後綴的長度。圖片

F數組的運用

那麼如今假設咱們已經獲得了F的全部值,咱們如何利用F數組求解呢?
咱們仍是先給出一個例子(筆者用了好長時間才構造出這一個比較典型的例子啊):
A="abaabaabbabaaabaabbabaab"
B="abaabbabaab"

固然讀者能夠經過手動模擬得出只有一個地方匹配
abaabaabbabaaabaabbabaab
那麼咱們根據手動模擬,一樣能夠計算出各個F的值字符串

B="a b a a b b a b a a b "
F= 0 0 1 1 2 0 1 2 3 4 5(2017.7.25 Update 這裏以前有一個錯誤,感謝@ 歌古道指正)(2017.7.29 Update 好吧,這裏原來還有一個錯誤,已經更正啦感謝@iwangtst)

咱們再用i表示當前A串要匹配的位置(即還未匹配),j表示當前B串匹配的位置(一樣也是還未匹配),補充一下,若i>0則說明i-1是已經匹配的啦(j同理)。
首先咱們仍是從0開始匹配:
此處輸入圖片的描述
此時,咱們發現,A的第5位和B的第5位不匹配(注意從0開始編號),此時i=5,j=5,那麼咱們看F[j-1]的值:

F[5-1]=2;

這說明咱們接下來的匹配只要從B串第2位開始(也就是第3個字符)匹配,由於前兩位已是匹配的啦,具體請看圖:
此處輸入圖片的描述
而後再接着匹配:
此處輸入圖片的描述
咱們又發現,A串的第13位和B串的第10位不匹配,此時i=13,j=10,那麼咱們看F[j-1]的值:

F[10-1]=4

這說明B串的0~3位是與當前(i-4)~(i-1)是匹配的,咱們就不須要從新再匹配這部分了,把B串向後移,從B串的第4位開始匹配:
此處輸入圖片的描述

這時咱們發現A串的第13位和B串的第4位依然不匹配
此處輸入圖片的描述
此時i=13,j=4,那麼咱們看F[j-1]的值:

F[4-1]=1

這說明B串的第0位是與當前i-1位匹配的,因此咱們直接從B串的第1位繼續匹配:
此處輸入圖片的描述
但此時B串的第1位與A串的第13位依然不匹配
此處輸入圖片的描述
此時,i=13,j=1,因此咱們看一看F[j-1]的值:

F[1-1]=0

好吧,這說明已經沒有相同的先後綴了,直接把B串向後移一位,直到發現B串的第0位與A串的第i位能夠匹配(在這個例子中,i=13)
此處輸入圖片的描述
再重複上面的匹配過程,咱們發現,匹配成功了!
此處輸入圖片的描述

這就是KMP算法的過程。
另外強調一點,當咱們將B串向後移的過程其實就是i++,而當咱們不動B,而是匹配的時候,就是i++,j++,這在後面的代碼中會出現,這裏先作一個說明。

最後來一個完整版的(話說作這些圖作了很久啊!!!!):
此處輸入圖片的描述

F數組的求解

既然已經用這麼多篇幅具體闡述瞭如何利用F數組求解,那麼如何計算出F數組呢?總不能暴力求解吧。

KMP的另一個巧妙的地方也就在這裏,它利用咱們上面用B匹配A的方法來計算F數組,簡單點來講,就是用B串匹配B串本身!
固然,由於B串==B串,因此若是直接按上面的匹配,那是毫無心義的(本身固然能夠徹底匹配本身啦),因此這裏要變一變。

由於上面已經講過一部分了,先給出計算F的代碼:

for (int i=1;i<m;i++)
{
    int j=F[i-1];
    while ((B[j+1]!=B[i])&&(j>=0))
        j=F[j];
    if (B[j+1]==B[i])
        F[i]=j+1;
    else
        F[i]=-1;
}

首先能夠肯定的幾點是:

1.F[0]=-1 (雖然說這裏應該是0,但爲了方便判越界,同時爲了方便判斷第0位與第i位,程序中這裏置爲-1)
2.這是一個從前日後的線性推導,因此在計算F[i]時能夠保證F[0]~F[i-1]都是已經計算出來的了
3.若以某一位結尾的子串不存在相同的前綴和後綴,這個位的F置爲-1(這裏置爲-1的緣由同第一條同樣)

重要!:另外,爲了在程序中表示方便,在接下來的說明中,F[i]=0表示最長相同前綴後綴長度爲1,即真實的最長相同前綴後綴=F[i]+1。(重要的內容要放大)
爲何要這樣設置呢,由於這時F[i]表明的就不只僅與先後綴長度有關了,它還表明着這個前綴的最後一個字符在子串B中的位置。

因此,以前上面列出的F值要變一下(這裏用'_'輔助對齊):

B="a _b a a b _b a b a a b "
F= -1 -1 0 0 1 -1 0 1 2 3 4

那麼,咱們一樣能夠推出,求解F的思路是:看F[i-1]這個最長相同前綴後綴的後面是否能夠接i,若能夠,則直接接上,若不能夠,下面再說。
舉個例子:
仍是以B="abaabbabaab"爲例,咱們看到第2個。

B="a b a a b b a b a a b"
F=-1 -1

此時這個a的前一個b的F值爲-1,因此此時a不能接在b的後面(b的相同最長前綴後綴是0啊),此時,j=-1,因此咱們判斷B[j+1]與B[2],即B[0]與B[2]是否同樣。同樣,因此F[2]=j+1=0(表明前0~2字符的最長相同前綴後綴的前綴結束處是B[0],長度爲0+1=1)。

再來看到第3個:

B="a b a a b b a b a a b"
F=-1 -1 0

開始時,j=F[3-1]=0,咱們發現B[j+1=1]!=B[i=3],因此j=F[j]=-1,此時B[j+1=0]==B[i=3],因此F[3]=j+1=0。

最後舉個例子,看到第4個

B="a b a a b b a b a a b"
F=-1 -1 0 0

j首先爲F[4-1]=0,咱們看到B[j+1=1]==B[i],因此F[i]=j+1=1。

後面的就請讀者本身慢慢推導了。再強調一遍,咱們這樣求出來的F值是該最長相同前綴後綴中的前綴的結束字符的數組位置(從0開始編號),若是要求最長相同前綴後綴的長度,要輸出F[i]+1。

代碼

求解F數組:

for (int i=1;i<m;i++)
{
    int j=F[i-1];
    while ((B[j+1]!=B[i])&&(j>=0))
        j=F[j];
    if (B[j+1]==B[i])
        F[i]=j+1;
    else
        F[i]=-1;
}

利用F數組尋找匹配,這裏咱們是每找到一個匹配就輸出其開始的位置:

while (i<n)
{
    if (A[i]==B[j])
    {
        i++;
        j++;
        if (j==m)
        {
            printf("%d\n",i-m+1);//注意,這裏輸出的位置是從1開始標號的,若是你要輸出從0開始標號的位置,應該是是i-m.這份代碼是我作一道題時寫的,那道題要求輸出的字符串位置從1開始標號.感謝@Draymonder指出了這個疏漏,更多內容請看評論區
            j=F[j-1]+1;
        }
    }
    else
    {
        if (j==0)
            i++;
        else
            j=F[j-1]+1;
    }
}

如下內容 Update at 2019.4.26

貼一個如今本身的寫法,不過這裏字符串是從 1 開始標號的,若是上面理解了的話不難轉化。

Nxt[0]=Nxt[1]=0;
for (int i=2,j=0;i<=m;i++){//構建 Next
    while (j&&T[j+1]!=T[i]) j=Nxt[j];
    if (T[j+1]==T[i]) ++j;Nxt[i]=j;
}
for (int i=1,j=0;i<=n;i++){//匹配
    while (j&&T[j+1]!=S[i]) j=Nxt[j];
    if (T[j+1]==S[i]) ++j;
    if (j==m) Mch[i]=1,j=Nxt[j];//匹配成功
}
相關文章
相關標籤/搜索