引言:
在文本編輯中,咱們常常要在一段文本中某個特定的位置找出 某個特定的字符或模式。
由此,便產生了字符串的匹配問題。
本文由簡單的字符串匹配算法開始,再到KMP算法,由淺入深,教你從頭至尾完全理解KMP算法。ios
來看算法導論一書上關於此字符串問題的定義:
假設文本是一個長度爲n的數組T[1...n],模式是一個長度爲m<=n的數組P[1....m]。
進一步假設P和T的元素都是屬於有限字母表Σ.中的字符。c++
依據上圖,再來解釋下字符串匹配問題。目標是找出全部在文本T=abcabaabcaabac中的模式P=abaa全部出現。
該模式僅在文本中出現了一次,在位移s=3處。位移s=3是有效位移。正則表達式
第一節、簡單的字符串匹配算法算法
簡單的字符串匹配算法用一個循環來找出全部有效位移,
該循環對n-m+1個可能的每個s值檢查條件P[1....m]=T[s+1....s+m]。數組
NAIVE-STRING-MATCHER(T, P)
1 n ← length[T]
2 m ← length[P]
3 for s ← 0 to n - m
4 do if P[1 ‥ m] = T[s + 1 ‥ s + m]
//對n-m+1個可能的位移s中的每個值,比較相應的字符的循環必須執行m次。
5 then print "Pattern occurs with shift" s函數
簡單字符串匹配算法,上圖針對文本T=acaabc 和模式P=aab。
上述第4行代碼,n-m+1個可能的位移s中的每個值,比較相應的字符的循環必須執行m次。
因此,在最壞狀況下,此簡單模式匹配算法的運行時間爲O((n-m+1)m)。spa
--------------------------------.net
下面我再來舉個具體例子,並給出一具體運行程序:
對於目的字串target是banananobano,要匹配的字串pattern是nano,的狀況,設計
下面是匹配過程,原理很簡單,只要先和target字串的第一個字符比較,
若是相同就比較下一個,若是不一樣就把pattern右移一下,
以後再從pattern的每個字符比較,這個算法的運行過程以下圖。
//index表示的每n次匹配的情形。blog
#include<iostream>
#include<string>
using namespace std;
int match(const string& target,const string& pattern)
{
int target_length = target.size();
int pattern_length = pattern.size();
int target_index = 0;
int pattern_index = 0;
while(target_index < target_length && pattern_index < pattern_length)
{
if(target[target_index]==pattern[pattern_index])
{
++target_index;
++pattern_index;
}
else
{
target_index -= (pattern_index-1);
pattern_index = 0;
}
}
if(pattern_index == pattern_length)
{
return target_index - pattern_length;
}
else
{
return -1;
}
}
int main()
{
cout<<match("banananobano","nano")<<endl;
return 0;
}
//運行結果爲4。
上面的算法進間複雜度是O(pattern_length*target_length),
咱們主要把時間浪費在什麼地方呢,
觀查index =2那一步,咱們已經匹配了3個字符,而第4個字符是不匹配的,這時咱們已經匹配的字符序列是nan,
此時若是向右移動一位,那麼nan最早匹配的字符序列將是an,這確定是不能匹配的,
以後再右移一位,匹配的是nan最早匹配的序列是n,這是能夠匹配的。
若是咱們事先知道pattern自己的這些信息就不用每次匹配失敗後都把target_index回退回去,
這種回退就浪費了不少沒必要要的時間,若是能事先計算出pattern自己的這些性質,
那麼就能夠在失配時直接把pattern移動到下一個可能的位置,
把其中根本不可能匹配的過程省略掉,
如上表所示咱們在index=2時失配,此時就能夠直接把pattern移動到index=4的狀態,
kmp算法就是今後出發。
第二節、KMP算法
2.一、 覆蓋函數(overlay_function)
覆蓋函數所表徵的是pattern自己的性質,可讓爲其表徵的是pattern從左開始的全部連續子串的自我覆蓋程度。
好比以下的字串,abaabcaba
因爲計數是從0始的,所以覆蓋函數的值爲0說明有1個匹配,對於從0仍是歷來開始計數是偏好問題,
具體請自行調整,其中-1表示沒有覆蓋,那麼何爲覆蓋呢,下面比較數學的來看一下定義,好比對於序列
a0a1...aj-1 aj
要找到一個k,使它知足
a0a1...ak-1ak=aj-kaj-k+1...aj-1aj
而沒有更大的k知足這個條件,就是說要找到儘量大k,使pattern前k字符與後k字符相匹配,k要儘量的大,
緣由是若是有比較大的k存在,而咱們選擇較小的知足條件的k,
那麼當失配時,咱們就會使pattern向右移動的位置變大,而較少的移動位置是存在匹配的,這樣咱們就會把可能匹配的結果丟失。
好比下面的序列,
在紅色部分失配,正確的結果是k=1的狀況,把pattern右移4位,若是選擇k=0,右移5位則會產生錯誤。
計算這個overlay函數的方法能夠採用遞推,能夠想象若是對於pattern的前j個字符,若是覆蓋函數值爲k
a0a1...ak-1ak=aj-kaj-k+1...aj-1aj
則對於pattern的前j+1序列字符,則有以下可能
⑴ pattern[k+1]==pattern[j+1] 此時overlay(j+1)=k+1=overlay(j)+1
⑵ pattern[k+1]≠pattern[j+1] 此時只能在pattern前k+1個子符組所的子串中找到相應的overlay函數,h=overlay(k),若是此時pattern[h+1]==pattern[j+1],則overlay(j+1)=h+1不然重複(2)過程.
下面給出一段計算覆蓋函數的代碼:
#include<iostream>
#include<string>
using namespace std;
void compute_overlay(const string& pattern)
{
const int pattern_length = pattern.size();
int *overlay_function = new int[pattern_length];
int index;
overlay_function[0] = -1;
for(int i=1;i<pattern_length;++i)
{
index = overlay_function[i-1];
//store previous fail position k to index;
while(index>=0 && pattern[i]!=pattern[index+1])
{
index = overlay_function[index];
}
if(pattern[i]==pattern[index+1])
{
overlay_function[i] = index + 1;
}
else
{
overlay_function[i] = -1;
}
}
for(i=0;i<pattern_length;++i)
{
cout<<overlay_function[i]<<endl;
}
delete[] overlay_function;
}
int main()
{
string pattern = "abaabcaba";
compute_overlay(pattern);
return 0;
}
運行結果爲:
-1
-1
0
0
1
-1
0
1
2
Press any key to continue
-------------------------------------
2.二、kmp算法
有了覆蓋函數,那麼實現kmp算法就是很簡單的了,咱們的原則仍是從左向右匹配,可是當失配發生時,咱們不用把target_index向回移動,target_index前面已經匹配過的部分在pattern自身就能體現出來,只要動pattern_index就能夠了。
當發生在j長度失配時,只要把pattern向右移動j-overlay(j)長度就能夠了。
若是失配時pattern_index==0,至關於pattern第一個字符就不匹配,
這時就應該把target_index加1,向右移動1位就能夠了。
ok,下圖就是KMP算法的過程(紅色便是採用KMP算法的執行過程):
另外一做者saturnman發現,在上述KMP匹配過程圖中,index=8和index=11處畫錯了。還有,anaven也早已發現,index=3處也畫錯了。很是感謝。但圖已沒法修改,見諒。
KMP 算法可在O(n+m)時間內完成所有的串的模式匹配工做。
ok,最後給出KMP算法實現的c++代碼:
#include<iostream>
#include<string>
#include<vector>
using namespace std;
int kmp_find(const string& target,const string& pattern)
{
const int target_length = target.size();
const int pattern_length = pattern.size();
int * overlay_value = new int[pattern_length];
overlay_value[0] = -1;
int index = 0;
for(int i=1;i<pattern_length;++i)
{
index = overlay_value[i-1];
while(index>=0 && pattern[index+1]!=pattern[i])
{
index = overlay_value[index];
}
if(pattern[index+1]==pattern[i])
{
overlay_value[i] = index +1;
}
else
{
overlay_value[i] = -1;
}
}
//match algorithm start
int pattern_index = 0;
int target_index = 0;
while(pattern_index<pattern_length&&target_index<target_length)
{
if(target[target_index]==pattern[pattern_index])
{
++target_index;
++pattern_index;
}
else if(pattern_index==0)
{
++target_index;
}
else
{
pattern_index = overlay_value[pattern_index-1]+1;
}
}
if(pattern_index==pattern_length)
{
return target_index-pattern_index;
}
else
{
return -1;
}
delete [] overlay_value;
}
int main()
{
string source = " annbcdanacadsannannabnna";
string pattern = " annacanna";
cout<<kmp_find(source,pattern)<<endl;
return 0;
}
//運行結果爲 -1.
第三節、kmp算法的來源
kmp如此精巧,那麼它是怎麼來的呢,爲何要三我的協力才能想出來。其實就算沒有kmp算法,人們在字符匹配中也能找到相同高效的算法。這種算法,最終至關於kmp算法,只是這種算法的出發點不是覆蓋函數,不是直接從匹配的內在原理出發,而使用此方法的計算的覆蓋函數過程序複雜且不易被理解,可是一但找到這個覆蓋函數,那之後使用同一pattern匹配時的效率就和kmp同樣了,其實這種算法找到的函數不該叫作覆蓋函數,由於在尋找過程當中根本沒有考慮是否覆蓋的問題。
說了這麼半天那麼這種方法是什麼呢,這種方法是就大名鼎鼎的肯定的有限自動機(Deterministic finite state automaton DFA),DFA可識別的文法是3型文法,又叫正規文法或是正則文法,既然能夠識別正則文法,那麼識別肯定的字串確定不是問題(肯定字串是正則式的一個子集)。對於如何構造DFA,是有一個完整的算法,這裏不作介紹了。在識別肯定的字串時使用DFA實在是大材小用,DFA能夠識別更加通用的正則表達式,而用通用的構建DFA的方法來識別肯定的字串,那這個overhead就顯得太大了。
kmp算法的難得之處是從字符匹配的問題自己特色出發,巧妙使用覆蓋函數這一表徵pattern自身特色的這一律念來快速直接生成識別字串的DFA,所以對於kmp這種算法,理解這種算法高中數學就能夠了,可是若是想從無到有設計出這種算法是要求有比較深的數學功底的。
第四節、精確字符匹配的常見算法的解析
KMP算法:
KMP就是串匹配算法
運用自動機原理
好比說
咱們在S中找P
設P={ababbaaba}
咱們將P對本身匹配
下面是求的過程:{依次記下匹配失敗的那一位}
[2]ababbaaba
.......ababbaaba[1]
[3]ababbaaba
.........ababbaaba[1]
[4]ababbaaba
.........ababbaaba[2]
[5]ababbaaba
.........ababbaaba[3]
[6]ababbaaba
................ababbaaba[1]
[7]ababbaaba
................ababbaaba[2]
[8]ababbaaba
..................ababbaaba[2]
[9]ababbaaba
..................ababbaaba[3]
獲得Next數組『0,1,1,2,3,1,2,2,3』
主過程:
[1]i:=1 j:=1
[2]若(j>m)或(i>n)轉[4]不然轉[3]
[3]若j=0或a[i]=b[j]則【inc(i)inc(j)轉[2]】不然【j:=next[j]轉2】
[4]若j>m則return(i-m)不然return -1;
若返回-1表示失敗,不然表示在i-m處成功
BM算法也是一種快速串匹配算法,KMP算法的主要區別是匹配操做的方向不一樣。雖然T右移的計算方法卻發生了較大的變化。
爲方便討論,T="dist :c->{dist稱爲滑動距離函數,它給出了正文中可能出現的任意字符在模式中的位置。函數 m – j j爲 dist(m+1 若c = tm
例如,pattern",則p)a)t)dist(= 2,r)n)BM算法的基本思想是:假設將主串中自位置i + dist(si)位置開始從新進行新一輪的匹配,其效果至關於把模式和主串向右滑過一段距離si),即跳過si)個字符而無需進行比較。
下面是一個S ="T="BM算法能夠大大加快串匹配的速度。
下面是KMP算法部分,把調用BM函數即可。
- #include <iostream>
- using namespace std;
- int Dist(char *t,char ch)
- {
- int len = strlen(t);
- int i = len - 1;
- if(ch == t[i])
- return len;
- i--;
- while(i >= 0)
- {
- if(ch == t[i])
- return len - 1 - i;
- else
- i--;
- }
- return len;
- }
- int BM(char *s,char *t)
- {
- int n = strlen(s);
- int m = strlen(t);
- int i = m-1;
- int j = m-1;
- while(j>=0 && i<n)
- {
- if(s[i] == t[j])
- {
- i--;
- j--;
- }
- else
- {
- i += Dist(t,s[i]);
- j = m-1;
- }
- }
- if(j < 0)
- {
- return i+1;
- }
- return -1;
- }
Horspool算法
這個算法是由R.Nigel Horspool在1980年提出的。其滑動思想很是簡單,就是從後往前匹配模式串,若在某一位失去匹配,此位對應的文本串字符爲c,那就將模式串向右滑動,使模式
串以前最近的c對準這一位,再重新從後往前檢查。那若是以前找不到c怎麼辦?那好極了,直接將整個模式串滑過這一位。
例如:文本串:abdabaca
模式串:baca倒數第2位失去匹配,模式串以前又沒有d,那模式串就能夠整個滑過,變成這樣:
文本串:abdabaca
模式串: baca發現倒數第1位就失去匹配,以前1位有c,那就向右滑動1位:
文本串:abdabaca
模式串: baca實現代碼:
- #include <iostream>
- #include <vector>
- #include <string>
- #include <cstdlib>
- using namespace std;
- int Horspool_match(const string & S,const string & M,int pos)
- {
- int S_len = S.size();
- int M_len = M.size();
- int Mi = M_len-1,Si= pos+Mi; //這裏的串的第1個元素下標是0
- if( (S_len-pos) < M_len )
- return -1;
- while ( (Mi>-1) && (Si<S_len) )
- {
- if (S[Si] == M[Mi])
- {
- --Mi;
- --Si;
- }
- else
- {
- do
- {
- Mi--;
- }
- while( (S[Si]!=M[Mi]) || (Mi>-1) );
- Mi = M_len - 1;
- Si += M_len - 1;
- }
- }
- if(Si < S_len)
- return(Si + 1);
- else
- return -1;
- }
- int main( )
- {
- string S="abcdefghabcdefghhiijiklmabc";
- string T="hhiij";
- int pos = Horspool_match(S,T,3);
- cout<<"/n"<<pos<<endl;
- system("pause");
- return 0;
- }
SUNDAY算法:
BM算法的改進的算法SUNDAY--Boyer-Moore-Horspool-Sunday Aglorithm
BM算法優於KMP
SUNDAY 算法描述:
字符串查找算法中,最著名的兩個是KMP算法(Knuth-Morris-Pratt)和BM算法(Boyer-Moore)。兩個算法在最壞狀況下均具備線性的查找時間。可是在實用上,KMP算法並不比最簡單的c庫函數strstr()快多少,而BM算法則每每比KMP算法快上3-5倍。可是BM算法還不是最快的算法,這裏介紹一種比BM算法更快一些的查找算法即Sunday算法。
例如咱們要在"substring searching algorithm"查找"search",剛開始時,把子串與文本左邊對齊:
substring searching algorithm
search
^
結果在第二個字符處發現不匹配,因而要把子串日後移動。可是該移動多少呢?這就是各類算法各顯神通的地方了,最簡單的作法是移動一個字符位置;KMP是利用已經匹配部分的信息來移動;BM算法是作反向比較,並根據已經匹配的部分來肯定移動量。這裏要介紹的方法是看緊跟在當前子串以後的那個字符(上圖中的 'i')。
顯然,無論移動多少,這個字符是確定要參加下一步的比較的,也就是說,若是下一步匹配到了,這個字符必須在子串內。因此,能夠移動子串,使子串中的最右邊的這個字符與它對齊。如今子串'search'中並不存在'i',則說明能夠直接跳過一大片,從'i'以後的那個字符開始做下一步的比較,以下圖:
substring searching algorithm
search
^
比較的結果,第一個字符就不匹配,再看子串後面的那個字符,是'r',它在子串中出如今倒數第三位,因而把子串向前移動三位,使兩個'r'對齊,以下:
substring searching algorithm
search
^
哈!此次匹配成功了!回顧整個過程,咱們只移動了兩次子串就找到了匹配位置,能夠證實,用這個算法,每一步的移動量都比BM算法要大,因此確定比BM算法更快。
- #include<iostream>
- #include<fstream>
- #include<vector>
- #include<algorithm>
- #include<string>
- #include<list>
- #include<functional>
- using namespace std;
- int main()
- {
- char *text=new char[100];
- text="substring searching algorithm search";
- char *patt=new char[10];
- patt="search";
- size_t temp[256];
- size_t *shift=temp;
- size_t patt_size=strlen(patt);
- cout<<"size : "<<patt_size<<endl;
- for(size_t i=0;i<256;i++)
- *(shift+i)=patt_size+1;//全部值賦於7,對這題而言
- for(i=0;i<patt_size;i++)
- *(shift+unsigned char(*(patt+i) ) )=patt_size-i;
- /* // 移動3步-->shift['r']=6-3=3;移動三步
- //shift['s']=6步,shitf['e']=5以此類推
- */
- size_t text_size=strlen(text);
- size_t limit=text_size-i+1;
- for(i=0;i<limit;i+=shift[text[i+patt_size] ] )
- if(text[i]==*patt)
- {
- /* ^13--這個r是位,從0開始算
- substring searching algorithm
- search
- searching-->這個s爲第10位,從0開始算
- 若是第一個字節匹配,那麼繼續匹配剩下的
- */
- char* match_text=text+i+1;
- size_t match_size=1;
- do{
- if(match_size==patt_size)
- cout<<"the no is "<<i<<endl;
- }while( (*match_text++)==patt[match_size++] );
- }
- cout<<endl;
- }
- delete []text;
- delete []patt;
- return 0;
- }
- //運行結果以下:
- /*
- size : 6
- the no is 10
- the no is 30
- Press any key to continue
- */
本文完,更多請參考:六(續)、從KMP算法一步一步談到BM算法。
頂
3