版權聲明:本文爲CSDN博主「Sirm23333」的原創文章,遵循 CC 4.0 BY-SA 版權協議,轉載請附上原文出處連接及本聲明。
原文連接:https://blog.csdn.net/qq_37969433/article/details/82947411html
本KMP原文最初寫於2年多前的2011年12月,因當時初次接觸KMP,思路混亂致使寫也寫得混亂。因此一直想找機會從新寫下KMP,但苦於一直以來對KMP的理解始終不夠,故才遲遲沒有修改本文。面試
KMP自己不復雜,但網上絕大部分的文章(包括本文的2011年版本)把它講混亂了。下面,我們從暴力匹配算法講起,隨後闡述KMP的流程 步驟、next 數組的簡單求解 遞推原理 代碼求解,接着基於next 數組匹配,談到有限狀態自動機,next 數組的優化,KMP的時間複雜度分析,最後簡要介紹兩個KMP的擴展算法。算法
全文力圖給你一個最爲完整最爲清晰的KMP,但願更多的人再也不被KMP折磨或糾纏,再也不被一些混亂的文章所混亂。有何疑問,歡迎隨時留言評論,thanks。數組
假設如今咱們面臨這樣一個問題:有一個文本串S,和一個模式串P,如今要查找P在S中的位置,怎麼查找呢?數據結構
若是用暴力匹配的思路,並假設如今文本串S匹配到 i 位置,模式串P匹配到 j 位置,則有:ide
int ViolentMatch(char* s, char* p) { int sLen = strlen(s); int pLen = strlen(p); int i = 0; int j = 0; while (i < sLen && j < pLen) { 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; } } //匹配成功,返回模式串p在文本串s中的位置,不然返回-1 if (j == pLen) return i - j; else return -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)ui
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),如此進行下去
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 的位置,讓模式串儘可能地移動到有效的位置。
int KmpSearch(char* s, char* p) { int i = 0; int j = 0; int sLen = strlen(s); int pLen = strlen(p); while (i < sLen && j < pLen) { //①若是j = -1,或者當前字符匹配成功(即S[i] == P[j]),都令i++,j++ if (j == -1 || s[i] == p[j]) { i++; j++; } else { //②若是j != -1,且當前字符匹配失敗(即S[i] != P[j]),則令 i 不變,j = next[j] //next[j]即爲j所對應的next值 j = next[j]; } } if (j == pLen) return i - j; else return -1; }
好比對於字符串aba來講,它有長度爲1的相同前綴後綴a;而對於字符串abab來
好比對於aba來講,第3個字符a以前的字符串ab中有長度爲0的相同前綴後綴,因此第3個字符a對應的next值爲0;而對於abab來講,第4個字符b以前的字符串aba中有長度爲1的相同前綴後綴a,因此第4個字符b對應的next值爲1(相同前綴後綴的長度爲k,k = 1)。
失配時,模式串向右移動的位數爲:已匹配字符數 - 失配字符的上一位字符所對應的最大長度值 |
下面,我們就結合以前的《最大長度表》和上述結論,進行字符串的匹配。若是給定文本串「BBC ABCDAB ABCDABCDABDE」,和模式串「ABCDABD」,如今要拿模式串去跟文本串匹配,以下圖所示:
經過上述匹配過程能夠看出,問題的關鍵就是尋找模式串中最大長度的相同前綴和後綴,找到了模式串中每一個字符以前的前綴和後綴公共部分的最大長度後,即可基於此匹配。而這個最大長度便正是next 數組要表達的含義。
由上文,咱們已經知道,字符串「ABCDABD」各個前綴後綴的最大公共元素長度分別爲:
並且,根據這個表能夠得出下述結論
把next 數組跟以前求得的最大長度表對比後,不難發現,next 數組至關於「最大長度值」 總體向右移動一位,而後初始值賦爲-1。意識到了這一點,你會驚呼原來next 數組的求解居然如此簡單:就是找最大對稱長度的前綴後綴,而後總體右移一位,初值賦爲-1(固然,你也能夠直接計算某個字符對應的next值,就是看這個字符以前的字符串中有多大長度的相同前綴後綴)。
換言之,對於給定的模式串:ABCDABD,它的最大長度表及next 數組分別以下:
根據最大長度表求出了next 數組後,從而有
失配時,模式串向右移動的位數爲:失配字符所在位置 - 失配字符對應的next 值 |
然後,你會發現,不管是基於《最大長度表》的匹配,仍是基於next 數組的匹配,二者得出來的向右移動的位數是同樣的。爲何呢?由於:
因此,你能夠把《最大長度表》看作是next 數組的雛形,甚至就把它當作next 數組也是能夠的,區別不過是怎麼用的問題。
接下來,我們來寫代碼求下next 數組。
基於以前的理解,可知計算next 數組的方法能夠採用遞推:
舉個例子,以下圖,根據模式串「ABCDABD」的next 數組可知失配位置的字符D對應的next 值爲2,表明字符D前有長度爲2的相同前綴和後綴(這個相同的前綴後綴即爲「AB」),失配後,模式串須要向右移動j - next [j] = 6 - 2 =4位。
向右移動4位後,模式串中的字符C繼續跟文本串匹配。
對於P的前j+1個序列字符:
void GetNext(char* p,int next[]) { int pLen = strlen(p); next[0] = -1; int k = -1; int j = 0; while (j < pLen - 1) { //p[k]表示前綴,p[j]表示後綴 if (k == -1 || p[j] == p[k]) { ++k; ++j; next[j] = k; } else { k = next[k]; } } }
用代碼從新計算下「ABCDABD」的next 數組,以驗證以前經過「最長相同前綴後綴長度值右移一位,而後初值賦爲-1」獲得的next 數組是否正確,計算結果以下表格所示:
從上述表格能夠看出,不管是以前經過「最長相同前綴後綴長度值右移一位,而後初值賦爲-1」獲得的next 數組,仍是以後經過代碼遞推計算求得的next 數組,結果是徹底一致的。
下面,咱們來基於next 數組進行匹配。
仍是給定文本串「BBC ABCDAB ABCDABCDABDE」,和模式串「ABCDABD」,如今要拿模式串去跟文本串匹配,以下圖所示:
在正式匹配以前,讓咱們來再次回顧下上文2.1節所述的KMP算法的匹配流程:
匹配過程如出一轍。也從側面佐證了,next 數組確實是只要將各個最大前綴後綴的公共元素的長度值右移一位,且把初值賦爲-1 便可。
咱們已經知道,利用next 數組進行匹配失配時,模式串向右移動 j - next [ j ] 位,等價於已匹配字符數 - 失配字符的上一位字符所對應的最大長度值。緣由是:
但爲什麼本文不直接利用next 數組進行匹配呢?由於next 數組很差求,而一個字符串的前綴後綴的公共元素的最大長度值很容易求。例如若給定模式串「ababa」,要你快速口算出其next 數組,乍一看,每次求對應字符的next值時,還得把該字符排除以外,而後看該字符以前的字符串中有最大長度爲多大的相同前綴後綴,此過程不夠直接。而若是讓你求其前綴後綴公共元素的最大長度,則很容易直接得出結果:0 0 1 2 3,以下表格所示:
而後這5個數字 所有總體右移一位,且初值賦爲-1,即獲得其next 數組:-1 0 0 1 2。
next 負責把模式串向前移動,且當第j位不匹配的時候,用第next[j]位和主串匹配,就像打了張「表」。此外,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 數組求法 void GetNextval(char* p, int next[]) { int pLen = strlen(p); next[0] = -1; int k = -1; int j = 0; while (j < pLen - 1) { //p[k]表示前綴,p[j]表示後綴 if (k == -1 || p[j] == p[k]) { ++j; ++k; //較以前next數組求法,改動在下面4行 if (p[j] != p[k]) next[j] = k; //以前只有這一行 else //由於不能出現p[j] = p[ next[j ]],因此當出現時須要繼續遞歸,k = next[k] = next[next[k]] next[j] = next[k]; } else { k = next[k]; } } }
利用優化事後的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數組爲:-1 0 0 -1 0 0,前綴後綴abc的next值都爲-1 0 0。
而後引用下以前3.1節的KMP代碼:
nt KmpSearch(char* s, char* p) { int i = 0; int j = 0; int sLen = strlen(s); int pLen = strlen(p); while (i < sLen && j < pLen) { //①若是j = -1,或者當前字符匹配成功(即S[i] == P[j]),都令i++,j++ if (j == -1 || s[i] == p[j]) { i++; j++; } else { //②若是j != -1,且當前字符匹配失敗(即S[i] != P[j]),則令 i 不變,j = next[j] //next[j]即爲j所對應的next值 j = next[j]; } } if (j == pLen) return i - j; else return -1; }
接下來,我們繼續拿以前的例子說明,整個匹配過程以下:
1. S[3]與P[3]匹配失敗。
2. S[3]保持不變,P的下一個匹配位置是P[next[3]],而next[3]=0,因此P[next[3]]=P[0]與S[3]匹配。
3. 因爲上一步驟中P[0]與S[3]仍是不匹配。此時i=3,j=next [0]=-1,因爲知足條件j==-1,因此執行「++i, ++j」,即主串指針下移一個位置,P[0]與S[4]開始匹配。最後j==pLen,跳出循環,輸出結果i - j = 4(即模式串第一次在文本串中出現的位置),匹配成功,算法結束。
「KMP的算法流程:
咱們發現若是某個字符匹配成功,模式串首字符的位置保持不動,僅僅是i++、j++;若是匹配失配,i 不變(即 i 不回溯),模式串會跳過匹配過的next [j]個字符。整個算法最壞的狀況是,當模式串首字符位於i - j的位置時才匹配成功,算法結束。
因此,若是文本串的長度爲n,模式串的長度爲m,那麼匹配過程的時間複雜度爲O(n),算上計算next的O(m)時間,KMP的總體時間複雜度爲O(m + n)。
KMP的匹配是從模式串的開頭開始匹配的,而1977年,德克薩斯大學的Robert S. Boyer教授和J Strother Moore教授發明了一種新的字符串匹配算法:Boyer-Moore算法,簡稱BM算法。該算法從模式串的尾部開始匹配,且擁有在最壞狀況下O(N)的時間複雜度。在實踐中,比KMP算法的實際效能高。
BM算法定義了兩個規則:
下面舉例說明BM算法。例如,給定文本串「HERE IS A SIMPLE EXAMPLE」,和模式串「EXAMPLE」,現要查找模式串是否在文本串中,若是存在,返回模式串在文本串中的位置。
1. 首先,"文本串"與"模式串"頭部對齊,從尾部開始比較。"S"與"E"不匹配。這時,"S"就被稱爲"壞字符"(bad character),即不匹配的字符,它對應着模式串的第6位。且"S"不包含在模式串"EXAMPLE"之中(至關於最右出現位置是-1),這意味着能夠把模式串後移6-(-1)=7位,從而直接移到"S"的後一位。
2. 依然從尾部開始比較,發現"P"與"E"不匹配,因此"P"是"壞字符"。可是,"P"包含在模式串"EXAMPLE"之中。由於「P」這個「壞字符」對應着模式串的第6位(從0開始編號),且在模式串中的最右出現位置爲4,因此,將模式串後移6-4=2位,兩個"P"對齊。
3. 依次比較,獲得 「MPLE」匹配,稱爲"好後綴"(good suffix),即全部尾部匹配的字符串。注意,"MPLE"、"PLE"、"LE"、"E"都是好後綴。
4. 發現「I」與「A」不匹配:「I」是壞字符。若是是根據壞字符規則,此時模式串應該後移2-(-1)=3位。問題是,有沒有更優的移法?
5. 更優的移法是利用好後綴規則:當字符失配時,後移位數 = 好後綴在模式串中的位置 - 好後綴在模式串中上一次出現的位置,且若是好後綴在模式串中沒有再次出現,則爲-1。
全部的「好後綴」(MPLE、PLE、LE、E)之中,只有「E」在「EXAMPLE」的頭部出現,因此後移6-0=6位。
能夠看出,「壞字符規則」只能移3位,「好後綴規則」能夠移6位。每次後移這兩個規則之中的較大值。這兩個規則的移動位數,只與模式串有關,與原文本串無關。
6. 繼續從尾部開始比較,「P」與「E」不匹配,所以「P」是「壞字符」,根據「壞字符規則」,後移 6 - 4 = 2位。由於是最後一位就失配,還沒有得到好後綴。
由上可知,BM算法不只效率高,並且構思巧妙,容易理解。
上文中,咱們已經介紹了KMP算法和BM算法,這兩個算法在最壞狀況下均具備線性的查找時間。但實際上,KMP算法並不比最簡單的c庫函數strstr()快多少,而BM算法雖然一般比KMP算法快,但BM算法也還不是現有字符串查找算法中最快的算法,本文最後再介紹一種比BM算法更快的查找算法即Sunday算法。
Sunday算法由Daniel M.Sunday在1990年提出,它的思想跟BM算法很類似:
下面舉個例子說明下Sunday算法。假定如今要在文本串"substring searching algorithm"中查找模式串"search"。
1. 剛開始時,把模式串與文本串左邊對齊:
substring searching algorithm
search
^
2. 結果發如今第2個字符處發現不匹配,不匹配時關注文本串中參加匹配的最末位字符的下一位字符,即標粗的字符 i,由於模式串search中並不存在i,因此模式串直接跳過一大片,向右移動位數 = 匹配串長度 + 1 = 6 + 1 = 7,從 i 以後的那個字符(即字符n)開始下一步的匹配,以下圖:
substring searching algorithm
search
^
3. 結果第一個字符就不匹配,再看文本串中參加匹配的最末位字符的下一位字符,是'r',它出如今模式串中的倒數第3位,因而把模式串向右移動3位(r 到模式串末尾的距離 + 1 = 2 + 1 =3),使兩個'r'對齊,以下:
substring searching algorithm
search
^
4. 匹配成功。
回顧整個過程,咱們只移動了兩次模式串就找到了匹配位置,緣於Sunday算法每一步的移動量都比較大,效率很高。完。
對以前混亂的文章給廣大讀者帶來的困擾表示致歉,對從新寫就後的本文即將給讀者帶來的清晰表示欣慰。但願大部分的初學者,甚至少部分的非計算機專業讀者也能看懂此文。有任何問題,歡迎隨時批評指正,thanks。
July、二零一四年八月二十二日晚九點。