KMP字符串搜索算法一

字符串匹配KMP算法詳解

1. 引言

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

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。

理清楚了暴力匹配算法的流程及內在的邏輯,咱們可以寫出暴力匹配的代碼,如下:

/**
     * 暴力破解法
     *
     * @param ss 主串
     * @param ps 模式串
     * @return 如果找到,返回在主串中第一個字符出現的下標,否則為-1
     */
    public int violentMatch(String ss, String ps) {
        char[] s = ss.toCharArray();
        char[] p = ps.toCharArray();

        int i = 0; // 主串的位置
        int j = 0; // 模式串的位置
        while (i < s.length && j < p.length) {
            if (s[i] == p[j]) {
                //①如果當前字符匹配成功(即s[i]==p[j]),則i++,j++
                i++;
                j++;
            } else {
                //②如果失敗(即s[i]!=p[j]),令i=i-j+1,j=0
                i = i - j + 1;
                j = 0;
            }
        }
        if (j == p.length) {
            return i - j;
        } else {
            return -1;
        }
    }
  1. 舉個例子,如果給定文本串S“BBC ABCDAB ABCDABCDABDE”,和模式串P“ABCDD”,現在要拿模式串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)

字符

(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)

字符

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

字符

(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)。

字符

(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個字符。

轉換成代碼表示,則是:

/**
     * KMP算法
     *
     * @param ss 主串
     * @param ps 模式串
     * @return 如果找到,返回在主串中第一個字符出現的下標,否則為-1
     */
    public static int KMP(String ss, String ps) {
        char[] s = ss.toCharArray();
        char[] p = ps.toCharArray();

        int i = 0; // 主串的位置
        int j = 0; // 模式串的位置
        int[] next = getNext(ps);
        while (i < s.length && j < p.length) {
            //①如果j=-1,或者當前字符匹配成功(即S[i]==P[j]),都令i++,j++
            if (j == -1 || s[i] == p[j]) { // 當j為-1時,要移動的是i,當然j也要歸0
                i++;
                j++;
            } else {
                //②如果j!=-1,且當前字符匹配失敗(即S[i]!=P[j]),則令i不變,j=next[j],j右移j-next[j]
                j = next[j];
            }
        }
        if (j == p.length) {
            return i - j;
        } else {
            return -1;
        }
    }

繼續拿之前的例子來說,當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]時,仔細觀察下圖:

image
image

[圖片上傳失敗...(image-1311-1518331239182)]

[圖片上傳失敗...(image-5981f8-1518331239182)]

可以得出以下規律:

當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:

image

像這種情況,令k = next[k],如果p[next[k]]==p[j],

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

image

觀察上圖,因為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 數組,代碼如下所示:

public int[] getNext(String ps) {
        char[] p = ps.toCharArray();
        int[] next = new int[p.length];
        next[0] = -1;
        int j = 0;
        int k = -1;
        while (j < p.length - 1) {
            //p[k]表示前綴,p[j]表示后綴
            if (k == -1 || p[k] == p[j]) {
                next[++j] = ++k;//即當p[k] == p[j]時,next[j+1] == next[j] + 1=k+1
            } else {
                k = next[k];
            }
        }
        return next;
    }

從上述表格可以看出,無論是之前通過“最長相同前綴后綴長度值右移一位,然后初值賦為-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 數組的代碼。

//優化過后的next數組求法
    public static int[] getNext(String ps) {
        char[] p = ps.toCharArray();
        int[] next = new int[p.length];
        next[0] = -1;
        int j = 0;
        int k = -1;
        while (j < p.length - 1) {
            //p[k]表示前綴,p[j]表示后綴
            if (k == -1 || p[j] == p[k]) {
                //較之前next數組求法,改動在下面4行
                if (p[++j] == p[++k]) {
                    next[j]=next[k];// 當兩個字符相等時要跳過
                } else {
                    next[j]=k;//之前只有這一行
                }
            } else {
                k = next[k];
            }
        }
        return next;
    }

利用優化過后的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代碼:

/**
     * KMP算法
     *
     * @param ss 主串
     * @param ps 模式串
     * @return 如果找到,返回在主串中第一個字符出現的下標,否則為-1
     */
    public static int KMP(String ss, String ps) {
        char[] s = ss.toCharArray();
        char[] p = ps.toCharArray();

        int i = 0; // 主串的位置
        int j = 0; // 模式串的位置
        int[] next = getNext(ps);
        while (i < s.length && j < p.length) {
            //①如果j=-1,或者當前字符匹配成功(即S[i]==P[j]),都令i++,j++
            if (j == -1 || s[i] == p[j]) { // 當j為-1時,要移動的是i,當然j也要歸0
                i++;
                j++;
            } else {
                //②如果j!=-1,且當前字符匹配失敗(即S[i]!=P[j]),則令i不變,j=next[j],j右移i-next[j]
                j = next[j];
            }
        }
        return j == p.length ? i - j : -1;
    }

//優化過后的next數組求法
    public static int[] getNext(String ps) {
        char[] p = ps.toCharArray();
        int[] next = new int[p.length];
        next[0] = -1;
        int j = 0;
        int k = -1;
        while (j < p.length - 1) {
            //p[k]表示前綴,p[j]表示后綴
            if (k == -1 || p[j] == p[k]) {
                //較之前next數組求法,改動在下面4行
                if (p[++j] == p[++k]) {
                    next[j]=next[k];// 當兩個字符相等時要跳過
                } else {
                    next[j]=k;//之前只有這一行
                }
            } else {
                k = next[k];
            }
        }
        return next;
    }

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

① 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(即模式串第一次在文本串中出現的位置),匹配成功,算法結束。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,030評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,310評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,951評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,796評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,566評論 6 407
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,055評論 1 322
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,142評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,303評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,799評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,683評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,899評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,409評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,135評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,520評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,757評論 1 282
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,528評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,844評論 2 372

推薦閱讀更多精彩內容

  • 文章大綱:1.KMP算法概念2.KMP算法中最核心的next[] 數組是如何生成的3.使用KMP算法 匹配字符串 ...
    檸檬烏冬面閱讀 823評論 0 3
  • 數據結構與算法--KMP算法查找子字符串 部分內容和圖片來自這三篇文章: 這篇文章、這篇文章、還有這篇他們寫得非常...
    sunhaiyu閱讀 1,750評論 1 21
  • 數據結構 第8講 KMP算法 講這個算法之前,我們首先了解幾個概念: 串:又稱字符串,是由零個或多個字符組成的有限...
    rainchxy閱讀 1,326評論 0 3
  • 引言 字符串匹配一直是計算機科學領域研究和應用的熱門領域,算法的改進研究一直是一個十分困難的課題。作為字符串匹配中...
    潮汐行者閱讀 1,669評論 2 6
  • 概述:本文主要在代碼層面上分析KMP的實現過程,如果您還不了解KMP的推導過程,請參考KMP(一) 模式匹配算法推...
    hehtao閱讀 2,503評論 1 4