KMP字符串匹配算法


去年冬天就接觸KMP算法了,但是聽的不明不白,遇到字符串匹配的題我大都直接使用string中的find解決了,但今天數據結構課又講了一下,我覺得有必要再來回顧一下。之前看過很多關於KMP的博客,有很多雖然很好,但是要么太專業,要么很難想象,這篇博客用了大量的圖示例子來說明,主要在於啟發,后面給出代碼說明。

 主要參考:http://www.ruanyifeng.com/blog/2013/05/Knuth%E2%80%93Morris%E2%80%93Pratt_algorithm.html

https://www.cnblogs.com/yjiyjige/p/3263858.html

https://www.cnblogs.com/aiguona/p/9133865.html

 

KMP算法引入:

KMP是三位大牛:D.E.Knuth、J.H.Morris和V.R.Pratt同時發現的

KMP算法要解決的問題就是在字符串(也叫主串)中的模式(pattern)定位問題。說簡單點就是我們平時常說的關鍵字搜索。模式串就是關鍵字(接下來稱它為P),如果它在一個主串(接下來稱為T)中出現,就返回它的具體位置,否則返回-1(常用手段)。

 

首先,對於這個問題有一個很單純的想法:從左到右一個個匹配,如果這個過程中有某個字符不匹配,就跳回去,將模式串向右移動一位。這有什么難的?

我們可以這樣初始化:

之后我們只需要比較i指針指向的字符和j指針指向的字符是否一致。如果一致就都向后移動,如果不一致,如下圖:

 

A和E不相等,那就把i指針移回第1位(假設下標從0開始),j移動到模式串的第0位,然后又重新開始這個步驟:

 

基於這個想法我們可以得到以下的程序:

 1 public static int bf(String ts, String ps)
 2 {
 3     int i = 0; // 主串的位置
 4     int j = 0; // 子串的位置
 5     while (i < t.length && j < p.length)
 6     {
 7         if (t[i] == p[j])/// 當兩個字符相同,就比較下一個
 8         {
 9             i++;
10             j++;
11         }
12         else
13         {
14             i = i - j + 1;///一旦不匹配,i后退
15             j = 0; ///j歸0
16         }
17 
18     }
19     if (j == p.length)
20     {
21         return i - j;///匹配成功返回子串在母串最先出現的位置
22     }
23     else
24     {
25         return -1;///不成功返回-1
26     }
27 
28 }

然而這並不是一種優秀的算法,因為會出現指針的回退,一旦匹配不成功就要退回子串的其實位置,而之前完成的部分匹配也將作廢,時間復雜度為O(n*m)。

而KMP算法卻能將時間復雜度優化為O(n+m),它是怎么做到的呢?我們再舉一個例子。

 

(1)對於已經匹配到這種狀態的兩個字符串:

一個基本事實是,當空格與D不匹配時,你其實知道前面六個字符是"ABCDAB"。KMP算法的想法是,設法利用這個已知信息,不要把"搜索位置"移回已經比較過的位置,繼續把它向后移,這樣就提高了效率。

 

(2)

 

怎么做到這一點呢?可以針對搜索詞,算出一張《部分匹配表》(Partial Match Table)。這張表是如何產生的,后面再介紹,這里只要會用就可以了。

 

(3)

已知空格與D不匹配時,前面六個字符"ABCDAB"是匹配的。查表可知,最后一個匹配字符B對應的"部分匹配值"為2,因此按照下面的公式算出向后移動的位數:

 移動位數 = 已匹配的字符數 - 對應的部分匹配值

因為 6 - 2 等於4,所以將搜索詞向后移動4位。

 

(4)

因為空格與C不匹配,搜索詞還要繼續往后移。這時,已匹配的字符數為2("AB"),對應的"部分匹配值"為0。所以,移動位數 = 2 - 0,結果為 2,於是將搜索詞向后移2位。

(5)

因為空格與A不匹配,繼續后移一位。

(6)

逐位比較,直到發現C與D不匹配。於是,移動位數 = 6 - 2,繼續將搜索詞向后移動4位。

(7)

逐位比較,直到搜索詞的最后一位,發現完全匹配,於是搜索完成。如果還要繼續搜索(即找出全部匹配),移動位數 = 7 - 0,再將搜索詞向后移動7位,這里就不再重復了。

 

下面介紹《部分匹配表》是如何產生的。

首先,要了解兩個概念:"前綴"和"后綴"。 "前綴"指除了最后一個字符以外,一個字符串的全部頭部組合;"后綴"指除了第一個字符以外,一個字符串的全部尾部組合。

"部分匹配值"就是"前綴"和"后綴"的最長的共有元素的長度。以"ABCDABD"為例,

- "A"的前綴和后綴都為空集,共有元素的長度為0;
  - "AB"的前綴為[A],后綴為[B],共有元素的長度為0;
  - "ABC"的前綴為[A, AB],后綴為[BC, C],共有元素的長度0;
  - "ABCD"的前綴為[A, AB, ABC],后綴為[BCD, CD, D],共有元素的長度為0;
  - "ABCDA"的前綴為[A, AB, ABC, ABCD],后綴為[BCDA, CDA, DA, A],共有元素為"A",長度為1;
  - "ABCDAB"的前綴為[A, AB, ABC, ABCD, ABCDA],后綴為[BCDAB, CDAB, DAB, AB, B],共有元素為"AB",長度為2;
  - "ABCDABD"的前綴為[A, AB, ABC, ABCD, ABCDA, ABCDAB],后綴為[BCDABD, CDABD, DABD, ABD, BD, D],共有元素的長度為0。

 為了記錄這些信息我們使用了一個next數組來記錄每一個字符的部分匹配值。

 

最后在對基本原理進行一下說明:

"部分匹配"的實質是,有時候,字符串頭部和尾部會有重復。比如,"ABCDAB"之中有兩個"AB",那么它的"部分匹配值"就是2("AB"的長度)。搜索詞移動的時候,第一個"AB"向后移動4位(字符串長度-部分匹配值),就可以來到第二個"AB"的位置。這也是我認為KMP算法最為厲害的地方,利用字符串自身具有的重復性避免了指針的回退!!!

 

kmp算法的核心即是計算子串F每一個位置之前的字符串的前綴和后綴公共部分的最大長度(不包括字符串本身,否則最大長度始終是字符串本身)。

獲得F每一個位置的最大公共長度之后,就可以利用該最大公共長度快速和字符串S比較。當每次比較到兩個字符串的字符不同時,我們就可以根據最大公共長度將字符串F向前移動(已匹配長度-最大公共長度)位,接着繼續比較下一個位置。事實上,字符串F的前移只是概念上的前移,只要我們在比較的時候從最大公共長度之后比較F和S即可達到字符串f前移的目的。

 

代碼說明:

獲得next數組:

 1 void cal_next(char *str, int *next, int len)
 2 {
 3     next[0] = -1;///next[0]初始化為-1,-1表示不存在相同的最大前綴和最大后綴
 4     int k = -1;///k初始化為-1
 5     for (int q = 1; q <= len-1; q++)
 6     {
 7         while (k > -1 && str[k + 1] != str[q])///如果下一個不同,那么k就變成next[k],注意next[k]是小於k的,無論k取任何值。
 8         {
 9             k = next[k];//往前回溯
10         }
11         if (str[k + 1] == str[q])//如果相同,k++
12         {
13             k = k + 1;
14         }
15         next[q] = k;///這個是把算的k的值(就是相同的最大前綴和最大后綴長)賦給next[q]
16     }
17 }

 kmp主函數:

 1 int KMP(char *str, int slen, char *ptr, int plen)
 2 {
 3     int *next = new int[plen];
 4     cal_next(ptr, next, plen);///計算next數組
 5     int k = -1;
 6     for (int i = 0; i < slen; i++)
 7     {
 8         while (k >-1&& ptr[k + 1] != str[i])///ptr和str不匹配,且k>-1(表示ptr和str有部分匹配)
 9         {
10             k = next[k];//往前回溯
11         }
12         if (ptr[k + 1] == str[i])
13         {
14             k = k + 1;
15         }
16         if (k == plen-1)///說明k移動到ptr的最末端
17         {
18             ///cout << "在位置" << i-plen+1<< endl;
19             ///k = -1;//重新初始化,尋找下一個
20             ///i = i - plen + 1;//i定位到該位置,外層for循環i++可以繼續找下一個(這里默認存在兩個匹配字符串可以部分重疊)
21             return i-plen+1;///返回相應的位置
22         }
23     }
24     return -1;
25 }

 代碼說明:

這一段代碼可以說是KMP算法的精髓, 這里給出以下說明。

1 while (k > -1 && str[k + 1] != str[q])
2         {
3             k = next[k];
4         }

看cal_next(..)函數:
首先我們看第一個while循環,它到底干了什么。
在此之前,我們先回到原程序。原程序里有一個大的for()循環,那這個for()循環是干嘛的?
這個for循環就是計算next[0],next[1],…next[q]…的值。
里面最后一句next[q]=k就是說明每次循環結束,我們已經計算了ptr的前(q+1)個字母組成的子串的“相同的最長前綴和最長后綴的長度”。這個“長度”就是k。
好,到此為止,假設循環進行到 第 q 次,即已經計算了next[q],我們是怎么計算next[q+1]呢?
比如我們已經知道ababab,q=4時,next[4]=2(k=2,表示該字符串的前5個字母組成的子串ababa存在相同的最長前綴和最長后綴的長度是3,所以k=2,next[4]=2。
這個結果可以理解成我們自己觀察算的,也可以理解成程序自己算的,這不是重點,重點是程序根據目前的結果怎么算next[5]的)。那么對於字符串ababab,我們計算next[5]的時候,此時q=5, k=2(上一步循環結束后的結果)。
那么我們需要比較的是str[k+1]和str[q]是否相等,其實就是str[1]和str[5]是否相等!,為啥從k+1比較呢,因為上一次循環中,我們已經保證了str[k]和str[q](注意這個q是上次循環的q)是相等的(這句話自己想想,很容易理解),所以到本次循環,我們直接比較str[k+1]和str[q]是否相等(這個q是本次循環的q)。
如果相等,那么跳出while(),進入if(),k=k+1,接着next[q]=k。即對於ababab,我們會得出next[5]=3。
如果不等,我們可以用”ababac“描述這種情況。不等,進入while()里面,進行k=next[k],這句話是說,在str[k + 1] != str[q]的情況下,我們往前找一個k,使str[k + 1]==str[q]。程序給出了一種找法,那就是 k = next[k]。
程序的意思是說,一旦str[k + 1] != str[q],即在后綴里面找不到時,我是可以直接跳過中間一段,跑到前綴里面找,next[k]就是相同的最長前綴和最長后綴的長度。所以,k=next[k]就變成,k=next[2],即k=0。
此時再比較str[0+1]和str[5]是否相等,不等,則k=next[0]=-1。跳出循環。

 

 1 #include<cstdio>
 2 #include<iostream>
 3 #include<cstring>
 4 #include<algorithm>
 5 using namespace std;
 6 void cal_next(char *str, int *next, int len)
 7 {
 8     next[0] = -1;
 9     int k = -1;
10     for (int q = 1; q <= len-1; q++)
11     {
12         while (k > -1 && str[k + 1] != str[q])
13         {
14             k = next[k];
15         }
16         if (str[k + 1] == str[q])
17         {
18             k = k + 1;
19         }
20         next[q] = k;
21     }
22 }
23 int KMP(char *str, int slen, char *ptr, int plen)
24 {
25     int *next = new int[plen];
26     cal_next(ptr, next, plen);
27     int k = -1;
28     for (int i = 0; i < slen; i++)
29     {
30         while (k >-1&& ptr[k + 1] != str[i])
31             k = next[k];
32         if (ptr[k + 1] == str[i])
33             k = k + 1;
34         if (k == plen-1)
35         {
36             //cout << "在位置" << i-plen+1<< endl;
37             //k = -1;//重新初始化,尋找下一個
38             //i = i - plen + 1;//i定位到該位置,外層for循環i++可以繼續找下一個(這里默認存在兩個匹配字符串可以部分重疊)
39             return i-plen+1;
40         }
41     }
42     return -1;
43 }
44 int main()
45 {
46     char *str = "bacbababadababacambabacaddababacasdsd";
47     char *ptr = "ababaca";
48     int a = KMP(str, 36, ptr, 7);
49     printf("%d",a);
50     return 0;
51 }

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM