字符串匹配KMP算法詳解

1. 引言

之前看過不少次KMP算法,一直以爲頗有用,但都沒有搞明白,一方面是網上不多有比較詳細的通俗易懂的講解,另外一方面也怪本身沒有沉下心來研究。最近在leetcode上又碰見字符串匹配的題目,以此爲契機,好好總結一下KMP算法。有何疑問,歡迎評論交流。html

 

2. 暴力匹配算法(傳統算法)

假設如今有這樣一個問題:有一個文本串S,和一個模式串P,如今要判斷S中是否有和P匹配的子串,並查找P在S中的位置,怎麼解決呢?算法

若是用暴力匹配的思路,並假設如今文本串S匹配到 i 位置,模式串P匹配到 j 位置,則有:數組

若是當前字符匹配成功(即S[i] == P[j]),則i++,j++,繼續匹配下一個字符;若是匹配失敗(即S[i]! = P[j]),令i = i - j + 1,j = 0,即每次匹配失敗時,i 回溯到上次開始匹配的下一個位置,j 被置爲0。函數

理清楚了暴力匹配算法的流程及內在的邏輯,我們能夠寫出暴力匹配的代碼,以下:post

 1 /**
 2      * 暴力破解法
 3      *
 4      * @param ss 主串
 5      * @param ps 模式串
 6      * @return 若是找到,返回在主串中第一個字符出現的下標,不然爲-1
 7      */
 8 
 9     public int violentMatch(String ss, String ps) {
10         char[] s = ss.toCharArray();
11         char[] p = ps.toCharArray();
12 
13         int i = 0; // 主串的位置
14         int j = 0; // 模式串的位置
15         while (i < s.length && j < p.length) {
16             if (s[i] == p[j]) {
17                 //①若是當前字符匹配成功(即s[i]==p[j]),則i++,j++
18                 i++;
19                 j++;
20             } else {
21                 //②若是失敗(即s[i]!=p[j]),令i=i-j+1,j=0
22                 i = i - j + 1;
23                 j = 0;
24             }
25         }
26         if (j == p.length) {
27             return i - j;
28         } else {
29             return -1;
30         }
31     }
  1. 舉個例子,若是給定文本串S「BBC ABCDAB ABCDABCDABDE」,和模式串P「ABCDABD」,如今要拿模式串P去跟文本串S匹配,整個過程以下所示:

(1)S[0]爲B,P[0]爲A,不匹配,故執行第②條指令:「若是失敗(即S[i]! = P[j]),令i = i - j + 1,j = 0」,S[1]跟P[0]匹配,至關於模式串要往右移動一位(i=1,j=0)優化

\

(2) S[1]跟P[0]仍是不匹配,繼續執行第②條指令:「若是失敗(即S[i]! = P[j]),令i = i - j + 1,j = 0」,S[2]跟P[0]匹配(i=2,j=0),從而模式串不斷的向右移動一位(不斷的執行「令i = i - j + 1,j = 0」,i從2變到4,j一直爲0)url

\

(3) 直到S[4]跟P[0]匹配成功(i=4,j=0),此時按照上面的暴力匹配算法的思路,轉而執行第①條指令:「若是當前字符匹配成功(即S[i] == P[j]),則i++,j++」,可得S[i]爲S[5],P[j]爲P[1],即接下來S[5]跟P[1]匹配(i=5,j=1)spa

\

        (4) S[5]跟P[1]匹配成功,繼續執行第①條指令:「若是當前字符匹配成功(即S[i] == P[j]),則i++,j++」,獲得S[6]跟P[2]匹配(i=6,j=2),如此進行下去。.net

\

(5) 直到S[10]爲空格字符,P[6]爲字符D(i=10,j=6),由於不匹配,從新執行第②條指令:「若是失敗(即S[i]! = P[j]),令i = i - j + 1,j = 0」,至關於S[5]跟P[0]匹配(i=5,j=0)。3d

\

(6)至此,咱們能夠看到,若是按照暴力匹配算法的思路,儘管以前文本串和模式串已經分別匹配到了S[9]、P[5],但由於S[10]跟P[6]不匹配,因此文本串回溯到S[5],模式串回溯到P[0],從而讓S[5]跟P[0]匹配。

\

而S[5]確定跟P[0]匹配失敗。爲何呢?由於在以前第4步匹配中,咱們已經得知S[5] = P[1] = B,而P[0] = A,即P[1] != P[0],故S[5]一定不等於P[0],因此回溯過去必然會致使失敗。那有沒有一種算法,讓i 不往回退,只須要移動j 便可呢?

答案是確定的。這種算法就是本文的主旨KMP算法,它利用以前已經部分匹配這個有效信息,保持i 不回溯,經過修改j 的位置,讓模式串儘可能地移動到有效的位置。

 

3. KMP算法

3.1 定義

KMP算法是一種改進的字符串匹配算法,由D.E.Knuth,J.H.Morris和V.R.Pratt同時發現,所以人們稱它爲克努特——莫里斯——普拉特操做(簡稱KMP算法)。KMP經常使用於在一個文本串S內查找一個模式串P 的出現位置,這個算法由Donald Knuth、Vaughan Pratt、James H. Morris三人於1977年聯合發表,故取這3人的姓氏命名此算法。KMP算法的關鍵是利用匹配失敗後的信息,儘可能減小模式串與主串的匹配次數以達到快速匹配的目的。具體實現就是實現一個next()函數,函數自己包含了模式串的局部匹配信息。時間複雜度O(m+n)。

下面先直接給出KMP的算法流程(若是感到一點點不適,不要緊,堅持下,稍後會有具體步驟及解釋):

假設如今文本串S匹配到 i 位置,模式串P匹配到 j 位置 若是j = -1,或者當前字符匹配成功(即S[i] == P[j]),都令i++,j++,繼續匹配下一個字符;若是j != -1,且當前字符匹配失敗(即S[i] != P[j]),則令 i 不變,j = next[j]。此舉意味着失敗時,模式串P相對於文本串S向右移動了j - next [j] 位。 換言之,當匹配失敗時,模式串向右移動的位數爲:失敗字符所在位置 - 失敗字符對應的next 值(next 數組的求解會在下文的3.3.3節中詳細闡述),即移動的實際位數爲:j - next[j],且此值大於等於1。 很快,你也會意識到next 數組各值的含義:若k=next[j],表明模式串P中當前字符以前的字符串中,最前面的k個字符和j以前的最後k個字符是同樣的。

若是用數學公式來表示是這樣的:

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

此也意味着在某個字符匹配失敗時,該字符對應的next 值會告訴你下一步匹配中,模式串應該跳到哪一個位置(跳到next [j] 的位置)。若是next [j] 等於0或-1,則跳到模式串的開頭字符,若next [j] = k 且 k > 0,表明下次匹配跳到j 以前的某個字符,而不是跳到開頭,且具體跳過了k 個字符。

若是用公式證實,是這樣的:

 

當S[i] != P[j]時

有S[i-j ~ i-1] == P[0 ~ j-1]

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

必然:S[i-k ~ i-1] == P[0 ~ k-1]

公式很無聊,能看明白就好了,不須要記住。

這一段只是爲了證實咱們爲何能夠直接將j移動到k而無須再比較前面的k個字符。

轉換成代碼表示,則是:

 

 1 /**
 2      * KMP算法
 3      *
 4      * @param ss 主串
 5      * @param ps 模式串
 6      * @return 若是找到,返回在主串中第一個字符出現的下標,不然爲-1
 7      */
 8     public static int KMP(String ss, String ps) {
 9         char[] s = ss.toCharArray();
10         char[] p = ps.toCharArray();
11 
12         int i = 0; // 主串的位置
13         int j = 0; // 模式串的位置
14         int[] next = getNext(ps);
15         while (i < s.length && j < p.length) {
16             //①若是j=-1,或者當前字符匹配成功(即S[i]==P[j]),都令i++,j++
17             if (j == -1 || s[i] == p[j]) { // 當j爲-1時,要移動的是i,固然j也要歸0
18                 i++;
19                 j++;
20             } else {
21                 //②若是j!=-1,且當前字符匹配失敗(即S[i]!=P[j]),則令i不變,j=next[j],j右移j-next[j]
22                 j = next[j];
23             }
24         }
25         if (j == p.length) {
26             return i - j;
27         } else {
28             return -1;
29         }
30     }

繼續拿以前的例子來講,當S[10]跟P[6]匹配失敗時,KMP不是跟暴力匹配那樣簡單的把模式串右移一位,而是執行第②條指令:「若是j != -1,且當前字符匹配失敗(即S[i] != P[j]),則令 i 不變,j = next[j]」,即j 從6變到2(後面咱們將求得P[6],即字符D對應的next 值爲2),因此至關於模式串向右移動的位數爲j - next[j](j - next[j] =6-2 = 4)。

 

\

向右移動4位後,S[10]跟P[2]繼續匹配。爲何要向右移動4位呢,由於移動4位後,模式串中又有個「AB」能夠繼續跟S[8]S[9]對應着,從而不用讓i 回溯。至關於在除去字符D的模式串子串中尋找相同的前綴和後綴,而後根據前綴後綴求出next 數組,最後基於next 數組進行匹配(不關心next 數組是怎麼求來的,只想看匹配過程是咋樣的,可直接跳到下文3.3.4節)。

 

 

\

3.2 KMP算法步驟

 

根據以上介紹,KMP算法的求解步驟歸納以下:

(1)尋找模式串的每一個子串前綴和後綴最長公共元素長度 

對於P = p0 p1 ...pj-1 pj,尋找模式串P中長度最大且相等的前綴和後綴。若是存在p0 p1 ...pk-1 pk = pj- k pj-k+1...pj-1 pj,那麼在包含pj的模式串中有最大長度爲k+1的相同前綴後綴。舉個例子,若是給定的模式串爲「abab」,那麼它的各個子串的前綴後綴的公共元素的最大長度以下表格所示:

\

好比對於字符串aba來講,它有長度爲1的相同前綴後綴a;而對於字符串abab來講,它有長度爲2的相同前綴後綴ab(相同前綴後綴的長度爲k + 1,k + 1 = 2)。

(2)求next數組 

next 數組考慮的是除當前字符外的最長相同前綴後綴,因此經過第(1)步驟求得各個前綴後綴的公共元素的最大長度後,只要稍做變形便可:將第①步驟中求得的值總體右移一位,而後初值賦爲-1,以下表格所示:

\

好比對於aba來講,第3個字符a以前的字符串ab中有長度爲0的相同前綴後綴,因此第3個字符a對應的next值爲0;而對於abab來講,第4個字符b以前的字符串aba中有長度爲1的相同前綴後綴a,因此第4個字符b對應的next值爲1(相同前綴後綴的長度爲k,k = 1)。

(3)根據next數組進行匹配

若匹配失配,j = next [j],模式串向右移動的位數爲:j - next[j]。換言之,當模式串的後綴pj-k pj-k+1, ..., pj-1 跟文本串si-k si-k+1, ..., si-1匹配成功,但pj 跟si匹配失敗時,由於next[j] = k,至關於在不包含p[j]的模式串中有最大長度爲k 的相同前綴和後綴,即p0 p1 ...pk-1 = pj-k pj-k+1...pj-1,故令j = next[j],從而讓模式串右移j - next[j] 位,使得模式串的前綴p0 p1, ..., pk-1對應着文本串 si-k si-k+1, ..., si-1,然後讓pk 跟si 繼續匹配。以下圖所示:

\

 

 

綜上,KMP的next 數組至關於告訴咱們:當模式串中的某個字符跟文本串中的某個字符匹配失配時,模式串下一步應該跳到哪一個位置。如模式串中在j 處的字符跟文本串在i 處的字符匹配失配時,下一步用next [j] 處的字符繼續跟文本串i 處的字符匹配,至關於模式串向右移動 j - next[j] 位。

接下來,分別具體解釋上述3個步驟。

 

3.3 算法解釋

3.3.1 尋找最長前綴後綴

若是給定的模式串是:「ABCDABD」,從左至右遍歷整個模式串,其各個子串的前綴後綴分別以下表格所示:\

也就是說,原模式串子串對應的各個前綴後綴的公共元素的最大長度表爲(下簡稱《最大長度表》):

 

\

 

3.3.2 基於《最大長度表》匹配

 

由於模式串的子串中首尾可能會有重複的字符,故可得出下述結論:

 

匹配失敗時,模式串向右移動的位數爲:已匹配字符數 - 失配字符的上一位字符所對應的最大長度值

 

下面,我們就結合以前的《最大長度表》和上述結論,進行字符串的匹配。若是給定文本串「BBC ABCDAB ABCDABCDABDE」,和模式串「ABCDABD」,如今要拿模式串去跟文本串匹配,以下圖所示:

\

(1) 由於模式串中的字符A跟文本串中的字符B、B、C、空格一開始就不匹配,因此沒必要考慮結論,直接將模式串不斷的右移一位便可,直到模式串中的字符A跟文本串的第5個字符A匹配成功:

\

(2)繼續日後匹配,當模式串最後一個字符D跟文本串匹配時失敗,顯而易見,模式串須要向右移動。但向右移動多少位呢?由於此時已經匹配的字符數爲6個(ABCDAB),而後根據《最大長度表》可得失配字符D的上一位字符B對應的長度值爲2,因此根據以前的結論,可知須要向右移動6 - 2 = 4 位。

\

(3)模式串向右移動4位後,發現C處再度失配,由於此時已經匹配了2個字符(AB),且上一位字符B對應的最大長度值爲0,因此向右移動:2 - 0 =2 位。

\

(4)A與空格失配,向右移動1 位。

\

(5)繼續比較,發現D與C 失配,故向右移動的位數爲:已匹配的字符數6減去上一位字符B對應的最大長度2,即向右移動6 - 2 = 4 位。

\

(6)經歷第5步後,發現匹配成功,過程結束。

\

經過上述匹配過程能夠看出,問題的關鍵就是尋找模式串中最大長度的相同前綴和後綴,找到了模式串中每一個字符以前的前綴和後綴公共部分的最大長度後,即可基於此匹配。而這個最大長度便正是next 數組要表達的含義。

3.3.3 根據《最大長度表》求next 數組

由上文,咱們已經知道,字符串「ABCDABD」各個前綴後綴的最大公共元素長度分別爲:

 

\

 

並且,根據這個表能夠得出下述結論:

      失配時,模式串向右移動的位數爲:已匹配字符數- 失配字符的上一位字符所對應的最大長度值

上文利用這個表和結論進行匹配時,咱們發現,當匹配到一個字符失配時,其實不必考慮當前失配的字符,更況且咱們每次失配時,都是看的失配字符的上一位字符對應的最大長度值。如此,便引出了next 數組。

給定字符串「ABCDABD」,可求得它的next 數組以下:

 

 

\

 

把next 數組跟以前求得的最大長度表對比後,不難發現,next 數組至關於「最大長度值」 總體向右移動一位,而後初始值賦爲-1。意識到了這一點,你會驚呼原來next 數組的求解居然如此簡單:就是找最大對稱長度的前綴後綴,而後總體右移一位,初值賦爲-1(固然,你也能夠直接計算某個字符對應的next值,就是看這個字符以前的字符串中有多大長度的相同前綴後綴)。

換言之,對於給定的模式串:ABCDABD,它的最大長度表及next 數組分別以下:

\

根據最大長度表求出了next 數組後,從而有

 

失配時,模式串向右移動的位數爲:失配字符所在位置- 失配字符對應的next 值

 

然後,你會發現,不管是基於《最大長度表》的匹配,仍是基於next 數組的匹配,二者得出來的向右移動的位數是同樣的。爲何呢?由於:

      根據《最大長度表》,失配時,模式串向右移動的位數 = 已經匹配的字符數 - 失配字符的上一位字符的最大長度值而根據《next 數組》,失配時,模式串向右移動的位數 = 失配字符的位置 - 失配字符對應的next 值 其中,從0開始計數時,失配字符的位置 = 已經匹配的字符數(失配字符不計數),而失配字符對應的next 值 =失配字符的上一位字符的最大長度值,兩相比較,結果必然徹底一致。

因此,你能夠把《最大長度表》看作是next 數組的雛形,甚至就把它當作next 數組也是能夠的,區別不過是怎麼用的問題。

3.3.4 經過代碼遞推計算next 數組

接下來,我們來寫代碼求下next 數組。

基於以前的理解,可知計算next 數組的方法能夠採用遞推:

(1)next數組的本質:

      若是對於值k,已有p0 p1, ..., pk-1 = pj-k pj-k+1, ..., pj-1,至關於next[j] = k。 此意味着什麼呢?究其本質,next[j] = k 表明p[j] 以前的模式串子串中,有長度爲k 的相同前綴和後綴。有了這個next 數組,在KMP匹配中,當模式串中j 處的字符匹配失敗時,下一步用next[j]處的字符繼續跟文本串匹配,至關於模式串向右移動j - next[j] 位。

舉個例子,以下圖,根據模式串「ABCDABD」的next 數組可知失配位置的字符D對應的next 值爲2,表明字符D前有長度爲2的相同前綴和後綴(這個相同的前綴後綴即爲「AB」),失配後,模式串須要向右移動j - next [j] = 6 - 2 =4位。

\

向右移動4位後,模式串中的字符C繼續跟文本串匹配。

\

(2)next數組的求解方法

 下面的問題是:已知next [0, ..., j],如何求出next [j + 1]呢?

   對於P的前j+1個序列字符,有兩種狀況:

①若p[k] == p[j]時,仔細觀察下圖:

能夠得出如下規律:

  當P[k] == P[j]時,

  有next[j+1] == next[j] + 1=k+1。(next[j] == k)

其實這個是能夠證實的:

  由於在P[j]以前已經有P[0 ~ k-1] == p[j-k ~ j-1]。(next[j] == k)

  這時候現有P[k] == P[j],咱們是否是能夠獲得P[0 ~ k-1] + P[k] == p[j-k ~ j-1] + P[j]。

  即:P[0 ~ k] == P[j-k ~ j],即next[j+1] == k + 1 == next[j] + 1。

這裏的公式不是很好懂,仍是看圖會容易理解些。

 

②若p[k ] ≠ p[j]時,好比下圖所示,對於字符串ABACDABABC:

 

 

像這種狀況,令k = next[k],若是p[next[k]]==p[j],

則next[j+1]=next[k]+1,不然繼續遞歸前綴索引k=next[k]。爲何是這樣子?你看下面應該就明白了。

 

 

  觀察上圖,由於p[k]≠p[j],即C和B不匹配,那麼就不能用next[j+1]=next[k]+1,此時只能用座標k以前的更短的子串來和j匹配,最笨的方法時用k以前的全部存在的子串來匹配,但考慮到next數組的含義,k對應的next[k]的表示k對應的字符以前的子串最大的相同前綴和後綴的長度,故直接將k左移到next[k]位置,繼續匹配ji

至關於在字符p[j+1]以前不存在長度爲k+1的前綴"p0 p1, …, pk-1 pk"跟後綴「pj-k pj-k+1, …, pj-1 pj"相等,那麼是否可能存在另外一個值t+1 < k+1,使得長度更小的前綴 「p0 p1, …, pt-1 pt」 等於長度更小的後綴 「pj-t pj-t+1, …, pj-1 pj」 呢?若是存在,那麼這個t+1 即是next[ j+1]的值,此至關於利用已經求得的next 數組(next [0, ..., k, ..., j])進行P串前綴跟P串後綴的匹配。

通常的文章或教材可能就此一筆帶過,但大部分的初學者可能仍是不能很好的理解上述求解next 數組的原理,故接下來,我再來着重說明下。

以下圖所示,假定給定模式串ABCDABCE,且已知next [j] = k(至關於「p0 pk-1」 = 「pj-k pj-1」 = AB,能夠看出k爲2),現要求next [j + 1]等於多少?由於pk = pj = C,因此next[j + 1] = next[j] + 1 = k + 1(能夠看出next[j + 1] = 3)。表明字符E前的模式串中,有長度k+1 的相同前綴後綴。

 

\

 

但若是pk != pj 呢?說明「p0 pk-1 pk」 ≠ 「pj-k pj-1 pj」。換言之,當pk != pj後,字符E前有多大長度的相同前綴後綴呢?很明顯,由於C不一樣於D,因此ABC 跟 ABD不相同,即字符E前的模式串沒有長度爲k+1的相同前綴後綴,也就不能再簡單的令:next[j + 1] = next[j] + 1 。因此,我們只能去尋找長度更短一點的相同前綴和後綴。

 

\

 

結合上圖來說,若能在前綴「 p0 pk-1 pk 」 中不斷的遞歸前綴索引k = next [k],找到一個字符pk’ 也爲D,表明pk’ = pj,且知足p0 pk'-1 pk' = pj-k' pj-1 pj,則最大相同的前綴後綴長度爲k' + 1,從而next [j + 1] = k’ + 1 = next [k' ] + 1。不然前綴中沒有D,則表明沒有相同的前綴後綴,next [j + 1] = 0。

那爲什麼遞歸前綴索引k = next[k],就能找到長度更短的相同前綴後綴呢?這又歸根到next數組的含義。

咱們拿前綴 p0 pk-1 pk 去跟後綴pj-k pj-1 pj匹配,若是pk 跟pj 失配,下一步就是用p[next[k]] 去跟pj 繼續匹配,若是p[ next[k] ]跟pj仍是不匹配,則須要尋找長度更短的相同前綴後綴,即下一步用p[ next[ next[k] ] ]去跟pj匹配。此過程至關於模式串的自我匹配,因此不斷的遞歸k = next[k],直到要麼找到長度更短的相同前綴後綴,要麼沒有長度更短的相同前綴後綴。以下圖所示:

 

 

\

 

 

因此,因最終在前綴ABC中沒有找到D,故E的next 值爲0:

 

模式串的後綴:ABDE
模式串的前綴:ABC
前綴右移兩位: ABC
 

 

讀到此,有的讀者可能又有疑問了,那可否舉一個能在前綴中找到字符D的例子呢?OK,我們便來看一個能在前綴中找到字符D的例子,以下圖所示:

 

 

\

 

給定模式串DABCDABDE,咱們很順利的求得字符D以前的「DABCDAB」的各個子串的最長相同前綴後綴的長度分別爲0 0 0 0 1 2 3,但當遍歷到字符D,要求包括D在內的「DABCDABD」最長相同前綴後綴時,咱們發現pj處的字符D跟pk處的字符C不同,換言之,前綴DABC的最後一個字符C 跟後綴DABD的最後一個字符D不相同,因此不存在長度爲4的相同前綴後綴。

怎麼辦呢?既然沒有長度爲4的相同前綴後綴,我們能夠尋找長度短點的相同前綴後綴,最終,因在p0處發現也有個字符D,p0 = pj,因此p[j]對應的長度值爲1,至關於E對應的next 值爲1(即字符E以前的字符串「DABCDABD」中有長度爲1的相同前綴和後綴)。

綜上,能夠經過遞推求得next 數組,代碼以下所示:

 

 1 public int[] getNext(String ps) {
 2         char[] p = ps.toCharArray();
 3         int[] next = new int[p.length];
 4         next[0] = -1;
 5         int j = 0;
 6         int k = -1;
 7         while (j < p.length - 1) {
 8             //p[k]表示前綴,p[j]表示後綴
 9             if (k == -1 || p[k] == p[j]) {
10                 next[++j] = ++k;//即當p[k] == p[j]時,next[j+1] == next[j] + 1=k+1
11             } else {
12                 k = next[k];
13             }
14         }
15         return next;
16     }

 

從上述表格能夠看出,不管是以前經過「最長相同前綴後綴長度值右移一位,而後初值賦爲-1」獲得的next 數組,仍是以後經過代碼遞推計算求得的next 數組,結果是徹底一致的。
仍是給定文本串「BBC ABCDAB ABCDABCDABDE」,和模式串「ABCDABD」,如今要拿模式串去跟文本串匹配,以下圖所示:

「假設如今文本串S匹配到 i 位置,模式串P匹配到 j 位置 若是j = -1,或者當前字符匹配成功(即S[i] == P[j]),都令i++,j++,繼續匹配下一個字符;若是j != -1,且當前字符匹配失敗(即S[i] != P[j]),則令 i 不變,j = next[j]。此舉意味着失配時,模式串P相對於文本串S向右移動了j - next [j] 位。 換言之,當匹配失敗時,模式串向右移動的位數爲:失配字符所在位置 - 失配字符對應的next 值,即移動的實際位數爲:j - next[j],且此值大於等於1。」
1. 最開始匹配時 P[0]跟S[0]匹配失敗 因此執行「若是j != -1,且當前字符匹配失敗(即S[i] != P[j]),則令 i 不變,j = next[j]」,因此j = -1,故轉而執行「若是j = -1,或者當前字符匹配成功(即S[i] == P[j]),都令i++,j++」,獲得i = 1,j = 0,即P[0]繼續跟S[1]匹配。 P[0]跟S[1]又失配,j再次等於-1,i、j繼續自增,從而P[0]跟S[2]匹配。P[0]跟S[2]失配後,P[0]又跟S[3]匹配。P[0]跟S[3]再失配,直到P[0]跟S[4]匹配成功,開始執行此條指令的後半段:「若是j = -1,或者當前字符匹配成功(即S[i] == P[j]),都令i++,j++」。

\

2. P[1]跟S[5]匹配成功,P[2]跟S[6]也匹配成功, ...,直到當匹配到P[6]處的字符D時失配(即S[10] != P[6]),因爲P[6]處的D對應的next 值爲2,因此下一步用P[2]處的字符C繼續跟S[10]匹配,至關於向右移動:j - next[j] = 6 - 2 =4 位。

\

3. 向右移動4位後,P[2]處的C再次失配,因爲C對應的next值爲0,因此下一步用P[0]處的字符繼續跟S[10]匹配,至關於向右移動:j - next[j] = 2 - 0 = 2 位。

\

4. 移動兩位以後,A 跟空格不匹配,模式串後移1 位。

\

5. P[6]處的D再次失配,由於P[6]對應的next值爲2,故下一步用P[2]繼續跟文本串匹配,至關於模式串向右移動 j - next[j] = 6 - 2 = 4 位。

\

6. 匹配成功,過程結束。

\

匹配過程如出一轍。也從側面佐證了,next 數組確實是只要將各個最大前綴後綴的公共元素的長度值右移一位,且把初值賦爲-1 便可。

3.3.6 基於《最大長度表》與基於《next 數組》等價

咱們已經知道,利用next 數組進行匹配失配時,模式串向右移動 j - next [ j ] 位,等價於已匹配字符數- 失配字符的上一位字符所對應的最大長度值。緣由是:

j 從0開始計數,那麼當數到失配字符時,j 的數值就是已匹配的字符數;因爲next 數組是由最大長度值表總體向右移動一位(且初值賦爲-1)獲得的,那麼失配字符的上一位字符所對應的最大長度值,即爲當前失配字符的next 值。

但爲什麼本文不直接利用next 數組進行匹配呢?由於next 數組很差求,而一個字符串的前綴後綴的公共元素的最大長度值很容易求。例如若給定模式串「ababa」,要你快速口算出其next 數組,乍一看,每次求對應字符的next值時,還得把該字符排除以外,而後看該字符以前的字符串中有最大長度爲多大的相同前綴後綴,此過程不夠直接。而若是讓你求其前綴後綴公共元素的最大長度,則很容易直接得出結果:0 0 1 2 3,以下表格所示:

\

而後這5個數字 所有總體右移一位,且初值賦爲-1,即獲得其next 數組:-1 0 0 1 2。

3.3.7 Next 數組與有限狀態自動機

next 負責把模式串向前移動,且當第j位不匹配的時候,用第next[j]位和主串匹配,就像打了張「表」。此外,next 也能夠看做有限狀態自動機的狀態,在已經讀了多少字符的狀況下,失配後,前面讀的若干個字符是有用的。

\

3.3.8 Next 數組的優化

行文至此,我們全面瞭解了暴力匹配的思路、KMP算法的原理、流程、流程之間的內在邏輯聯繫,以及next 數組的簡單求解(《最大長度表》總體右移一位,而後初值賦爲-1)和代碼求解,最後基於《next 數組》的匹配,看似洋洋灑灑,清晰透徹,但以上忽略了一個小問題。

好比,若是用以前的next 數組方法求模式串「abab」的next 數組,可得其next 數組爲-1 0 0 1(0 0 1 2總體右移一位,初值賦爲-1),當它跟下圖中的文本串去匹配的時候,發現b跟c失配,因而模式串右移j - next[j] = 3 - 1 =2位。

\

右移2位後,b又跟c失配。事實上,由於在上一步的匹配中,已經得知p[3] = b,與s[3] = c失配,而右移兩位以後,讓p[ next[3] ] = p[1] = b 再跟s[3]匹配時,必然失配。問題出在哪呢?

\

問題出在不應出現p[j] = p[ next[j] ]。爲何呢?理由是:當p[j] != s[i] 時,下次匹配必然是p[ next [j]] 跟s[i]匹配,若是p[j] = p[ next[j] ],必然致使後一步匹配失敗(由於p[j]已經跟s[i]失配,而後你還用跟p[j]等同的值p[next[j]]去跟s[i]匹配,很顯然,必然失配),因此不能容許p[j] = p[ next[j ]]。若是出現了p[j] = p[ next[j] ]咋辦呢?若是出現了,則須要再次遞歸,即令next[j] = next[ next[j] ]。

因此,我們得修改下求next 數組的代碼。

 1 //優化事後的next數組求法
 2     public static int[] getNext(String ps) {
 3         char[] p = ps.toCharArray();
 4         int[] next = new int[p.length];
 5         next[0] = -1;
 6         int j = 0;
 7         int k = -1;
 8         while (j < p.length - 1) {
 9             //p[k]表示前綴,p[j]表示後綴
10             if (k == -1 || p[j] == p[k]) {
11                 //較以前next數組求法,改動在下面4行
12                 if (p[++j] == p[++k]) {
13                     next[j]=next[k];// 當兩個字符相等時要跳過
14                 } else {
15                     next[j]=k;//以前只有這一行
16                 }
17             } else {
18                 k = next[k];
19             }
20         }
21         return next;
22     }

 

利用優化事後的next 數組求法,可知模式串「abab」的新next數組爲:-1 0 -1 0。可能有些讀者會問:原始next 數組是前綴後綴最長公共元素長度值右移一位, 而後初值賦爲-1而得,那麼優化後的next 數組如何快速心算出呢?實際上,只要求出了原始next 數組,即可以根據原始next 數組快速求出優化後的next 數組。仍是以abab爲例,以下表格所示:

\

 

只要出現了p[next[j]]=p[j]的狀況,則把next[j]的值再次遞歸。例如在求模式串「abab」的第2個a的next值時,若是是未優化的next值的話,第2個a對應的next值爲0,至關於第2個a失配時,下一步匹配模式串會用p[0]處的a再次跟文本串匹配,必然失配。因此求第2個a的next值時,須要再次遞歸:next[2]=next[next[2]]=next[0]=-1(此後,根據優化後的新next值可知,第2個a失配時,執行「若是j=-1,或者當前字符匹配成功(即S[i]==P[j]),都令i++,j++,繼續匹配下一個字符」),同理,第2個b對應的next值爲0。

對於優化後的next數組能夠發現一點:若是模式串的後綴跟前綴相同,那麼它們的next值也是相同的,例如模式串abcabc,它的前綴後綴都是abc,其優化後的next數組爲:-100-100,前綴後綴abc的next值都爲-100。

 

完整的KMP代碼:

 

 1 /**
 2      * KMP算法
 3      *
 4      * @param ss 主串
 5      * @param ps 模式串
 6      * @return 若是找到,返回在主串中第一個字符出現的下標,不然爲-1
 7      */
 8     public static int KMP(String ss, String ps) {
 9         char[] s = ss.toCharArray();
10         char[] p = ps.toCharArray();
11 
12         int i = 0; // 主串的位置
13         int j = 0; // 模式串的位置
14         int[] next = getNext(ps);
15         while (i < s.length && j < p.length) {
16             //①若是j=-1,或者當前字符匹配成功(即S[i]==P[j]),都令i++,j++
17             if (j == -1 || s[i] == p[j]) { // 當j爲-1時,要移動的是i,固然j也要歸0
18                 i++;
19                 j++;
20             } else {
21                 //②若是j!=-1,且當前字符匹配失敗(即S[i]!=P[j]),則令i不變,j=next[j],j右移i-next[j]
22                 j = next[j];
23             }
24         }
25         return j == p.length ? i - j : -1;
26     }
27 
28 //優化事後的next數組求法
29     public static int[] getNext(String ps) {
30         char[] p = ps.toCharArray();
31         int[] next = new int[p.length];
32         next[0] = -1;
33         int j = 0;
34         int k = -1;
35         while (j < p.length - 1) {
36             //p[k]表示前綴,p[j]表示後綴
37             if (k == -1 || p[j] == p[k]) {
38                 //較以前next數組求法,改動在下面4行
39                 if (p[++j] == p[++k]) {
40                     next[j]=next[k];// 當兩個字符相等時要跳過
41                 } else {
42                     next[j]=k;//以前只有這一行
43                 }
44             } else {
45                 k = next[k];
46             }
47         }
48         return next;
49     }

 

接下來,我們繼續拿以前的例子說明,整個匹配過程以下:

① S[3]與P[3]匹配失敗。

\

② S[3]保持不變,P的下一個匹配位置是P[next[3]],而next[3]=0,因此P[next[3]]=P[0]與S[3]匹配。

\

③ 因爲上一步驟中P[0]與S[3]仍是不匹配。此時i=3,j=next [0]=-1,因爲知足條件j==-1,因此執行「++i, ++j」,即主串指針下移一個位置,P[0]與S[4]開始匹配。最後j==pLen,跳出循環,輸出結果i - j = 4(即模式串第一次在文本串中出現的位置),匹配成功,算法結束。

\

 

 

參考:

(1)https://www.2cto.com/kf/201606/518714.html字符串匹配KMP算法的理解(詳細)

(2)http://www.cnblogs.com/yjiyjige/p/3263858.html (原創)詳解KMP算法

相關文章
相關標籤/搜索