模式匹配是數據結構中的基本運算之一,它在很多地方都得到了應用,字符串模式匹配指的是:找出模式串在一個較長的字符串中出現的位置。有兩個字符串target 和pattern,字符串target 稱爲目標串,字符串pattern 稱爲模式串,要求找出pattern在target 中首次出現的位置。一旦模式串pattern 在目標串target 中找到,就稱發生一次匹配。例如,目標串target=“banana”,模式串pattern=“nan",則匹配結果是2。常用的模式匹配方法有樸素模式匹配和KMP模式匹配。樸素模式匹配法很容易理解,就是將pattern和target一個一個字符比對,如果這個字符匹配了,則pattern和target都後移一位,繼續比對下一個字符;如果這個字符不匹配,則pattern起始位置歸零,target的起始位置移到上次匹配起始位置的下一位,然後重新開始新一輪的匹配。這裏舉個例子來簡要說明樸素模式匹配的過程:設目標串target=”ababcabcac",模式串pattern=“abcac",設置兩個指針i,j分別指向target和pattern,開始時,i指向target[0],j指向pattern[0],顯然pattern[0] 與target[0]匹配,i和j都後移一位,i指向target[1],j指向pattern[1], pattern[1] 與target[1]也匹配,i和j又都後移一位,i指向target2],j指向pattern[2], 但是pattern[2] 與target[2]不相等,也就不匹配了,至此這一輪的匹配結束,開始下一輪的匹配過程。
常用的模式匹配方法有樸素模式匹配和KMP模式匹配。樸素模式匹配法很容易理解,就是將pattern和target一個一個字符比對,如果這個字符匹配了,則pattern和target都後移一位,繼續比對下一個字符;如果這個字符不匹配,則pattern起始位置歸零,target的起始位置移到上次匹配起始位置的下一位,然後重新開始新一輪的匹配。這裏舉個例子來簡要說明樸素模式匹配的過程:
設目標串target=”ababcabcac",模式串pattern=“abcac",設置兩個指針i,j分別指向target和pattern,開始時,i指向target[0],j指向pattern[0],顯然pattern[0] 與target[0]匹配,i和j都後移一位,i指向target[1],j指向pattern[1], pattern[1] 與target[1]也匹配,i和j又都後移一位,i指向target2],j指向pattern[2],
但是pattern[2] 與target[2]不相等,也就不匹配了,至此這一輪的匹配結束,開始下一輪的匹配過程。
I = 0 |
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
a |
b |
a |
b |
c |
a |
b |
c |
a |
c |
J = 0 |
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
a |
b |
c |
b |
c |
|
|
|
|
|
下一輪匹配過程開始時,i從上一次匹配開始的位置0後移一位,那這裏i從1開始,j已然要從0開始,因爲target[1]不等於pattern[0],不匹配,那麼ij要從起始位置後移一位。
0 |
i = 1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
a |
b |
a |
b |
c |
a |
b |
c |
a |
c |
|
J = 0 |
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
|
a |
b |
c |
a |
c |
|
|
|
|
開始第三輪匹配,此時i=2,j=0. pattern[0]=target[2], pattern[1]=target[3],pattern[2]=target[4],pattern[3]=target[5],q前面幾個都字符匹配,但是pattern[4]!=target[6],失配,因此繼續下一輪匹配,i從起始位置後移一位,移到3上
0 |
1 |
i = 2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
a |
b |
a |
b |
c |
a |
b |
c |
a |
c |
|
|
j = 0 |
1 |
2 |
3 |
4 |
5 |
6 |
7 |
|
|
a |
b |
c |
a |
c |
|
|
|
開始第四輪匹配,此時i=3,j=0,
0 |
1 |
2 |
i = 3 |
4 |
5 |
6 |
7 |
8 |
9 |
a |
b |
a |
b |
c |
a |
b |
c |
a |
c |
|
|
|
j = 0 |
1 |
2 |
3 |
4 |
5 |
6 |
|
|
|
a |
b |
c |
a |
c |
|
|
0 |
1 |
2 |
3 |
4 |
i = 5 |
6 |
7 |
8 |
9 |
a |
b |
a |
b |
c |
a |
b |
c |
a |
c |
|
|
|
|
|
j = 0 |
1 |
2 |
3 |
4 |
|
|
|
|
|
a |
b |
c |
a |
c |
由此可以看出,雖然樸素模式匹配最終能找到模式串在目標串中的位置,但是匹配的過程過於繁瑣,例如在第三輪匹配過程中,前四個字符恰好匹配,但第五個字符不匹配,導致第四次匹配過程又要重頭開始,效率過於低下。
這裏給出一份樸素模式匹配的代碼,供大家參考 (此代碼已編譯通過,正常運行)
- //description:樸素模式匹配算法
- //author:hust_luojun
- //data:2014-7-17
- #include <iostream>
- #include <cstring>
- using namespace std;
- int main()
- {
- int simple_pattern_matching(char target[],char pattern[]);
- char target[]={"ababcabcacb"}; //target爲目標串
- char pattern[]={"abcac"}; //pattern爲模式串
- int pos=simple_pattern_matching(target,pattern);
- cout<<"target string: "<<target<<endl;
- cout<<"pattern string: "<<pattern<<endl;
- cout<<"the postition of pattern is: "<<pos<<endl;
- return 0;
- }
- int simple_pattern_matching(char target[],char pattern[])
- {
- int i=0;
- int j=0;
- int target_length=strlen(target); //目標串的長度
- int pattern_length=strlen(pattern); //模式串的長度
- while(i<target_length && j<pattern_length)
- {
- if(target[i]==pattern[j]) //逐位比較,若相等則比較下一位
- {
- i++;
- j++;
- }
- else //逐位比較,若不等則i從上一次開始比較的位置加1,j取0
- {
- i=i-j+1;
- j=0;
- }
- }
- if(j==pattern_length) //若j等於模式串的長度,說明匹配成功,返回模式串在目標串中的位置,若匹配不成功則返回-1
- return i-j;
- else
- return -1;
- }
樸素模式匹配算法過程易於理解,在很多應用場合效率也較高。此時,算法的時間複雜度爲O(n+m)。其中n和m分別爲目標串和模式的長度。然而,當目標串中存在多個和模式串中“部分匹配”的子串時,會引起指針i的多次回溯。因此,在最壞情況下的時間複雜度爲O(n*m)。那麼有沒有更好的匹配方法呢?當然有,就是接下來要介紹的KMP模式匹配方法。
KMP算法是一種可以在時間複雜度O(n+m)完成匹配查找的算法,它相對於樸素匹配算法的改進在於:當某一趟匹配失敗時,i不需要回溯到上次開始匹配位置的下一位,而是停留在失配時的那一位上,j也不用回溯到0,而是回溯到一個儘量靠右的位置,爲了找出這個儘量靠右的位置,這裏引入next[ ]數組,k=next[j], j爲模式串失配時的指向,在pattern[0....j-1]內找出滿足pattern[0.....k-1]==pattern[j-k....j-1] 的最大k值,下次匹配時j就回溯到k位置上。
樸素匹配那個例子中,進行第三趟匹配時,在最後一個字符上失配,此時i=6,j=4,進行第四趟匹配時,若採用KMP算法,i不需要從3開始,j不需要從0開始。由於模式串的前四個字符已經匹配,可以得知target[3,4,5]=="bca",故可以利用這個部分匹配串的信息,pattern[0]==a,不可能target[3],target[4]匹配,但可以跟target[5]匹配,不過這個匹配或不匹配的信息是可以由第三趟匹配過程得知的,故第四趟匹配時,i可以直接爲6,j直接爲2,這樣就跳過了中間一些不必要的比較過程,節省了時間。
KMP匹配過程如下:
第一趟:
a |
b |
a |
b |
c |
a |
b |
c |
a |
c |
a |
b |
c |
a |
c |
|
|
|
|
|
第二趟:
a |
b |
a |
b |
c |
a |
b |
c |
a |
c |
|
a |
b |
c |
a |
c |
|
|
|
|
a |
b |
a |
b |
c |
a |
b |
c |
a |
c |
|
|
a |
b |
c |
a |
c |
|
|
|
a |
b |
a |
b |
c |
a |
b |
c |
a |
c |
|
|
|
|
|
a |
b |
c |
a |
c |
在第四趟時就完成匹配了。下一次匹配時,j的起始比較位置j=k=next[j],那麼如何求next[j]呢?這裏直接給出公式,有興趣的童鞋可以自己推到一下
next[j]=-1 (j=0);
next[j]=0 (j=1);
next[j]=max{k} (其中k爲滿足下式的最大值 pattern[0....k-1]==pattern[j-k....j-1])
可見next[j]只於模式串有關,而與目標串無關
Pattern[j] |
|
|
|
|
|
|
|
|
|
a |
b |
a |
b |
c |
a |
b |
c |
a |
c |
Next[j] |
|
|
|
|
|
|
|
|
|
-1 |
0 |
0 |
1 |
2 |
0 |
1 |
2 |
0 |
1 |
因此KMP算法的思想就是:在匹配過程中,若發生不匹配的情況,如果next[j] ≥ 0,則目標串的指針i不變,將模式串的指針j 移動到next[j]的位置繼續進行匹配;如果next[j] == -1,則將i右移1位,並將j置0,繼續進行比較。
下面給出一份KMP算法的參考代碼(此代碼已編譯通過,正常運行),若大家有更好的實現代碼,歡迎分享,共同學習
- //description:KMP模式匹配算法
- //author:hust_luojun
- //data:2014-7-17
- #include <iostream>
- #include <cstring>
- using namespace std;
- int main()
- {
- int kmp_pattern_matching(char target[],char pattern[]); //KMP函數聲明
- char target[]={"ababcabcacb"}; //target爲目標串
- char pattern[]={"abcac"}; //pattern爲模式串
- int pos=kmp_pattern_matching(target,pattern); //調用KMP函數,返回模式串的位置
- cout<<"target string: "<<target<<endl;
- cout<<"pattern string: "<<pattern<<endl;
- cout<<"the postition of pattern is: "<<pos<<endl;
- return 0;
- }
- int kmp_pattern_matching(char target[],char pattern[])
- {
- int get_next(char pattern[]); // 求next[j]大小的函數聲明
- int i = 0;
- int j = 0;
- int target_length = strlen(target); //目標串的長度
- int pattern_length = strlen(pattern); //模式串的長度
- while(i < target_length && j < pattern_length)
- {
- if(target[i]==pattern[j] || j==-1) //逐個字符匹配,若相等則i和j分別後移一位
- {
- i++;
- j++;
- }
- else
- {
- j = get_next(pattern); //逐個字符匹配,若不相等,i保持失配使的值,j的值取next[j]
- }
- }
- if(j==pattern_length) //當j等於模式串的長度時,說明匹配成功,返回模式串在目標串中的位置
- return i-j;
- else
- return -1; //匹配不成功時,返回-1
- }
- int get_next(char pattern[]) //求next[j]數組,本函數中,用k替代了next[j],但本質是相同的,k=next[j]
- {
- bool compare(char pattern[],int k,int j);
- int pattern_length = strlen(pattern);
- int j;
- int k;
- int temp;
- for(j=0;j<pattern_length;j++)
- {
- if(j==0) //next[0]==-1; next[1]=0
- k=-1;
- else if(j==1)
- k=0;
- else
- {
- for(temp=j-1;temp>=1;temp--) //求next[j] (j>=2)的最大k值
- {
- if (compare(pattern,temp,j))
- k=temp;
- break;
- }
- }
- return k;
- }
- }
- bool compare(char pattern[],int k,int j)
- {
- int s=0;
- int t=j-k;
- for(;s<=k-1&&t<=j-1;s++,t++) //比較pattern[0....k-1]與pattern[j-k....j-1]是否相等,相等則返回true,不等則返回false
- if(pattern[s]!=pattern[t])
- return false;
- else
- return true;
- }
運行結果如下:
本文參考了張曉芳老師文章的部分內容,在此表示感謝!
/*****碼字不易,轉載請註明出處*****/