字符串精確匹配KMP算法思想演變

引言

字符串匹配一直是計(jì)算機(jī)科學(xué)領(lǐng)域研究和應(yīng)用的熱門領(lǐng)域,算法的改進(jìn)研究一直是一個(gè)十分困難的課題。作為字符串匹配中的經(jīng)典算法,KMP算法一直以較高的效率和富有美感的構(gòu)思聞名。本文希望由傳統(tǒng)的精確匹配算法入手,層層推進(jìn),研究KMP算法思想的演變歷程。

1.傳統(tǒng)的精確匹配

1.1 精確匹配的含義

字符串的精確匹配就是在文本T中找出模式P的精確副本。這是一個(gè)要么完全匹配要么、完全不匹配的方法。如果模式P和T的一個(gè)子字符串非常類似,但不相同,就拒絕部分匹配。
我們首先約定一些符號(hào):文本T是一系列符號(hào)、字符或字母,|T|表示T的長度,T(i)是T中位置i上的字符,T(i…j)是T中從位置i開始到位置j結(jié)束的子字符串,模式P(指在T中尋找匹配的字串)和文本T中的第一個(gè)字符在位置0上。另外,正則表達(dá)式a^n表示字符串a(chǎn)….a,其中包含n個(gè)a。

1.2 強(qiáng)制匹配

字符串精確匹配最簡單的算法即是直觀的“暴力破解”,這種稱為強(qiáng)制匹配的算法從文本T的第一個(gè)字母和模式P的第一個(gè)字母開始比較P和T,如果不匹配,就從T的第二個(gè)字符開始和模式P的第一個(gè)字母匹配,以此類推,不保留在后續(xù)嘗試中可能有用的信息。偽代碼十分常見,就不在此贅述了。
在最壞情況下,強(qiáng)制匹配的執(zhí)行時(shí)間是O(|T||P|)。

1.3 強(qiáng)制匹配的改進(jìn)

對于強(qiáng)制匹配算法,1992年Hancart提出了一種稱為not-so-naive算法的改進(jìn)。該算法從P的第二個(gè)字符開始比較,直到P的結(jié)尾,最后比較第一個(gè)字符,所以比較過程中的字符順序是P(1),P(2),…….,P(|P|-1),P(0)。
記錄下P的前兩個(gè)字符相等的信息,并在匹配過程中使用。要區(qū)分兩種情況:P(0)=P(1)和P(0)!=P(1)。在第一種情況下,如果P(1)!=T(i+1),就給文本索引i遞增2,因?yàn)镻(0)!=T(i+1);否則,i就遞增1。這類似于第二種情況中的P(1)=T(i+1)。這樣,就可以移動(dòng)兩個(gè)位置。
匹配從第二個(gè)字符開始。如果在P(1)和T(i+1)之間又一個(gè)不匹配的字符,則只要P的前兩個(gè)字符是相同的,P就可以移動(dòng)兩個(gè)位置,然后開始下一次迭代,因?yàn)檫@種不匹配意味著P(0)和T(i+1)也是不同的。但是,當(dāng)內(nèi)層循環(huán)中出現(xiàn)不匹配之后,模式只移動(dòng)一個(gè)位置。 而P的前兩個(gè)字符不同時(shí),采取的措施則相反。
在最壞情況下,該算法的執(zhí)行時(shí)間是O(|T||P|),但如Hancart所述,它的平均執(zhí)行性能比起KMP、Boyer-Moore等字符串匹配算法要好。

2. KMP算法

2.1 可以改進(jìn)什么?

強(qiáng)制算法的效率很低,因?yàn)樗谡也坏狡ヅ涞淖址螅涯J絇移動(dòng)一個(gè)位置。為了加快算法的執(zhí)行速度,Hancart的算法允許移動(dòng)兩個(gè)字符。但是,我們應(yīng)該需要一種算法,可以將P向右移動(dòng)多個(gè)位置,同時(shí)不遺漏任何匹配。
強(qiáng)制算法效率不高的根源是進(jìn)行了多余的比較。要避免這種冗余,應(yīng)該注意模式P在其開頭到不匹配的字符之間包含的相同的子字符串,利用這一事實(shí),可以把P向右移動(dòng)多個(gè)位置,之后開始下一次掃描。

2.2 如何改進(jìn)

從上文可以看出,一般情況下,當(dāng)T(K+1)與P(j+1)發(fā)生不匹配,而P(0…j)都已經(jīng)完成匹配時(shí),我們可以嘗試在P(0…j)中尋找與其后綴相匹配的前綴,假設(shè)匹配的前后綴字串的長度都是len,即P(0…len-1)和P( j-len+1….j )是兩個(gè)相同的字串,前綴從P的開頭起始,后綴以P的結(jié)尾結(jié)束,長度都是len,它們有相同的內(nèi)容。
因?yàn)镻(0….j)已經(jīng)完成了匹配,故T(K-len+1…K)與P(j-len+1….j)是匹配的,而P(j-len+1….j)與P(0…len-1)是匹配的。所以P(0…len-1)與T(K-len+1….K)是匹配的,當(dāng)T(K+1)與P(j+1)不匹配時(shí),我們可以不用像強(qiáng)制算法要求那樣將P(0)與T(K-j+2)重新開始匹配,可以將P(len-1)與T(K)對齊,從P(len)與T(K+1)開始比較。
由之前的論證可以知道,這樣是沒有錯(cuò)誤的,P(0…len-1)已經(jīng)完成匹配。而由于P(len…j-len)中在之前的子串匹配中已知無法與T(K-len+1…K)中某一子串匹配,故無法完成模式匹配,可知P(len-1)與T(K)對齊沒有遺漏或是跳過可能的模式匹配的情況。
在匹配過程中,P與T會(huì)多次不匹配,此時(shí)就需要移動(dòng)P或T使之到指定的位置,上述的信息將會(huì)使用多次。因此,P應(yīng)該進(jìn)行預(yù)處理。重要的是,在這種方法中,只使用有關(guān)P的信息,T中字符的配置無關(guān)緊要。

定義表next:

  1. 當(dāng)j=0時(shí),next[ j ]= -1;
  2. 如果k存在,next[ j ]= max{k:0< k <j,P[0...k-1] = P[j-k...j-1] };
  3. 其他,next[ j ]= 0 。

也就是說,next[ j ]表示子字符串P(0…j-1)中與相同字符串前綴匹配的最長后綴的長度。
條件k<j表示前綴也是一個(gè)正確的后綴,但這里由于P[0...k-1],我們不接受同是前綴和后綴的子串。沒有這個(gè)條件,P(0...2)=aab 的 next[2] 應(yīng)該是2,因?yàn)閍a既是aa的前綴也是后綴,但有了這個(gè)條件,next[2] = 1而不是2。
同是應(yīng)該注意重疊的情況,比如ababa最長匹配的后綴是aba,長度為3。
假設(shè)我們現(xiàn)在已經(jīng)獲得了處理模式P得到的next數(shù)組,至于處理方式,我們稍后再談。由強(qiáng)制匹配算法的偽代碼我們可以較容易的得到Knuth-Morris-Pratt算法的偽代碼:

1.Kunth-MorrisPratt(模式 P,文本 T)
2.    findNext(P,next);
3.    i = j = 0;
4.    while i <= |T| - |P|
5.        while j == -1 or (j<|P| and T[ i ] == P[ j ])
6.            i++;
7.            j++;
8.        if j == |P|
9.            return 在i - |P|處的匹配;
10.        j = next[ j ];
11.    return  沒有匹配;
````
i 標(biāo)記了文本T正在匹配的字符位置,j 標(biāo)記了模式P正在匹配的字符位置。當(dāng) j == -1時(shí),正在進(jìn)行模式P的第一個(gè)字符匹配,無字符完成匹配,因此進(jìn)入匹配過程。當(dāng) j < |P| 時(shí),說明匹配未完成, 嘗試對比P[ i ] == P[ j ]。若匹配,則更新i 和 j 的值,執(zhí)行下一對字符的匹配;若失敗,則此次匹配失敗,跳出循環(huán)。跳出循環(huán)后,判斷 j 是否等于 |P|,若等于,則說明P[0…|P|-1]都已經(jīng)完成匹配,即模式P完成精確匹配,從文本T的 i - |P| 處開始達(dá)成精確匹配。若不等于,則匹配未完成,且在P[ j ] 和 T[ i ]處失敗,根據(jù)之前的論述,此時(shí)我們可以查詢next數(shù)組,將P[next[ j ]]與T[ i ]對齊,然后繼續(xù)執(zhí)行匹配直到 i>|T|-|P| ,此時(shí)文本T已比較完,沒有可與模式P匹配的字串,匹配失敗。
注意,在比較過程 i 只會(huì)遞增或是不變,不會(huì)減少。
###2.3 求next數(shù)組
表next仍然沒有確定,我們可以使用強(qiáng)制算法來確定它,對于短模式而言,效率并不算低。還可以使用KMP算法提高確定next的效率。 
    next包含P的匹配前綴中最長后綴的長度,即P的一些部分與P的其他部分匹配。但匹配問題已經(jīng)使用KMP算法解決了,在這種情況下,P再次與其自身匹配。但是,KMP使用的是目前仍未知的next。所以,必須修改KMP算法,使之使用已經(jīng)找到的值確定next的值。設(shè)next [0] = -1,假定next[ 0 ],…next[i-1],應(yīng)該考慮兩種情況:     
 
+ 在第一種情況下,我們要找出匹配前綴的最長后綴,只要把字符P[i-1]與對應(yīng)于位置next[i-1]的后綴關(guān)聯(lián)起來,當(dāng)P[i-1]=P[next[i-1]]時(shí),它為真: 
    在這種情況下,當(dāng)前的后綴比前面找到的后綴多一個(gè)字符,所以next[ i ] = next[ i-1 ]+1。
+  在第二種情況下,P[i-1] != P[next[ i-1 ]]。但這只是一個(gè)不匹配的字符,不匹配可以用表next做處理,這就是要確定它的原因。因?yàn)镻[next[i-1]]是一個(gè)不匹配的字符,所以要檢查next[next[i-1]],確定P[i-1]是否匹配,如果匹配,就給next[i]賦值next[next[i-1]]+1。 
    否則,就比較P[i-1]和P[next[next[next[i-1]]]],如果字符匹配,就使next[ i ] = next[next[next[i-1]]]+1,否則,繼續(xù)搜索,直到找到一個(gè)匹配,或達(dá)到P的開頭為止。     

確定表next的算法如下:
````
1.findNext(模式 P,表 next)
2.    next[ 0 ] = -1;
3.    i = 0;
4.    j = -1;
5.    while i < |P|
6.        while j==0 或 i < |P| 且 P[i] == P[j]
7.            i++;
8.            j++;
9.            next[ i ] = j;
10.        j = next[j];
````
###2.3 next數(shù)組的改進(jìn)
如果去除不必要的比較,就可以改進(jìn)Knuth-Morris-Pratt算法。如果在字符T[i]和P[j]處出現(xiàn)不匹配,下一次就應(yīng)該嘗試匹配字符T[i]和P[next[j]+1],但如果P[j]=P[next[j]+1],則還會(huì)發(fā)生不匹配,這意味著一次多余的比較。
因此,我們應(yīng)該重新設(shè)計(jì)表next,去除這種多余的比較。這里可以使用擴(kuò)展next定義的方法,再加上一個(gè)條件,得到一個(gè)更強(qiáng)健的next列表:
1. 當(dāng)j=0時(shí),next[j]=-1;
2. 如果K存在的話,next[j]=max{k:0<k<j,P[0...k-1]=P[j-k...j-1],
P[k+1]!=P[j]};
3. 其他,next[ j ]= 0。   

為了計(jì)算nextS,算法findNext()需要考慮新加的條件,略作修改:
````
1.findNextS(模式 P , 表 nextS)
2.    nextS[0] = -1;
3.    i = 0;
4.    j = -1;
5.    while i < |P|
6.        while j==-1 或 i < |P| 且 P[ i ] == P[ j ]
7.            i++;
8.            j++;
9.            if P[i] != P[j]
10.                nextS[i] = j;
11.            else nextS[i] = nextS[j];
12.        j = nextS[j];
````
### 3.運(yùn)行時(shí)間分析
為了評估Kunth-MorrisPratt()的運(yùn)行時(shí)間,注意外層循環(huán)執(zhí)行了O(|T|)次。而因?yàn)樵诿看蔚难h(huán)迭代中,i都遞增,根據(jù)外層循環(huán)的條件,i的最大值是|T|-|P|,所以內(nèi)層循環(huán)至多執(zhí)行|T|-|P|。但對于不匹配的字符T[ i ],j賦予新值的次數(shù)是k < |P|。此時(shí),P中第一不匹配的字符與字符T[i+k]對齊。
對于i,可以執(zhí)行|P|次比較,但每個(gè)i不一定都會(huì)執(zhí)行|P|次比較,只有第|P|個(gè)i才會(huì)如此。所以不成功的比較次數(shù)最多為P(|T|/|P|)=|T|。給這個(gè)數(shù)字加上至多|T|-|P|次成功的比較次數(shù),就得到了運(yùn)行時(shí)間O(|T|)。
而findNext()與Kunth-MorrisPratt()十分相似,所以,可以推測出next可以在O(|P|)時(shí)間內(nèi)確定。Kunth-MorrisPratt()中外層while循環(huán)的運(yùn)行時(shí)間是O(T),所以Kunth-MorrisPratt算法,包括findNext()在內(nèi),執(zhí)行時(shí)間是O(|T|+|P|)。
注意,在分析這個(gè)算法的復(fù)雜度時(shí),我們未考慮文本T和模式P中的字符,也就是說,該復(fù)雜度是獨(dú)立于組成P和T的不同字符數(shù)。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,825評論 6 546
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 99,814評論 3 429
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,980評論 0 384
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 64,064評論 1 319
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 72,779評論 6 414
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 56,109評論 1 330
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,099評論 3 450
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 43,287評論 0 291
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,799評論 1 338
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 41,515評論 3 361
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 43,750評論 1 375
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,221評論 5 365
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,933評論 3 351
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,327評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,667評論 1 296
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 52,492評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 48,703評論 2 380

推薦閱讀更多精彩內(nèi)容