前幾天,忽然聽到一位剛剛面試完應聘者的同事吐槽到「如今的程序員基本功怎麼這麼差,連一個簡單的KMP算法都搞不定,還好意思開那麼高的薪水"。聽到這裏,筆者默默的翻出《數據結構》,打開google。本文正是在這樣的背景下對KMP算法的複習與整理。html
該算法是一種改進的字符串匹配算法,由D.E.Knuth與V.R.Pratt和J.H.Morris同時發現,所以稱之爲KMP算法。此算法能夠在O(n+m)的時間數量級上完成串的模式匹配操做。程序員
舉例來講,有一個字符串"BBC ABCDAB ABCDABCDABDE",我想知道,裏面是否包含另外一個字符串"ABCDABD"?面試
首先,字符串"BBC ABCDAB ABCDABCDABDE"的第一個字符與搜索字符串"ABCDABD"的第一個字符,進行比較。由於B與A不匹配,因此搜索詞後移一位。算法
由於B與A不匹配,搜索字符串再日後移。數組
就這樣,直到字符串有一個字符,搜索字符串的第一個字符相同爲止。網絡
接着比較字符串和搜索字符串的下一個字符,仍是相同。數據結構
直到字符串有一個字符,與搜索字符串對應的字符不相同爲止。函數
這時,最天然地方式就是將搜索字符串整個後移一位,再從頭逐個比較。這樣作雖然可行,可是效率不好,由於你要把"搜索位置"移到已經比較過的位置,重比一遍。其算法時間複雜度即爲O(m*n)。優化
一個基本事實是,當空格與D不匹配時,你其實知道前面六個字符是"ABCDAB"。KMP算法的關鍵思想就是,設法利用這個已知信息,不要把"搜索位置"移回已經比較過的位置,繼續把它向後移,這樣就提升了效率。google
怎麼作到這一點呢?能夠針對搜索字符串,算出一張《部分匹配表》(Partial Match Table)。這張表是如何產生的,後面再介紹,這裏只要會用就能夠了。
已知空格與D不匹配時,前面六個字符"ABCDAB"是匹配的。查表可知,最後一個匹配字符B對應的"部分匹配值"爲2,所以按照下面的公式算出向後移動的位數:
右移位數 = 已匹配的字符數 - 對應的部分匹配值
6-2=4, 則將搜索字符串後移4位。
由於空格與C不匹配,搜索字符串還要繼續日後移。這時,已匹配的字符數爲2("AB"),對應的"部分匹配值"爲0。因此,移動位數 = 2 - 0,結果爲 2,因而將搜索字符串向後移2位。
由於空格與A不匹配,繼續後移一位。
逐位比較,直到發現C與D不匹配。因而,移動位數 = 6 - 2,繼續將搜索字符串向後移動4位
逐位比較,直到搜索字符串的最後一位,發現徹底匹配,因而搜索完成。若是還要繼續搜索(即找出所有匹配),移動位數 = 7 - 0,再將搜索字符串向後移動7位,這裏就再也不重複了。
從上面的匹配過程,咱們發現部分匹配表是KMP算法的關鍵所在,解下來讓咱們看一下部分匹配表是如何生成的。
首先,咱們須要瞭解兩個概念:"前綴"和"後綴"。 "前綴"指除了最後一個字符之外,一個字符串的所有頭部組合;"後綴"指除了第一個字符之外,一個字符串的所有尾部組合。
字符串「string」爲例,則「string」的前綴即爲: 「s", "st", "str", "stri", "strin"。其後綴即爲: "g", "ng", "ing", "ring", "tring"。
"部分匹配值"就是"前綴"和"後綴"的最長的共有元素的長度。以"ABCDABD"爲例,
字符串 | 前綴 | 後綴 | 部分匹配值 |
A | 空集 | 空集 | 0 |
AB | A | B | 0 |
ABC | A, AB | C, BC | 0 |
ABCD | A, AB, ABC | D, CD, BCD | 0 |
ABCDA | A, AB, ABC, ABCD | A, DA, CDA, BCDA, | 1 |
ABCDAB | A, AB, ABC, ABCD, ABCDA | B, AB, DAB, CDAB, BCDAB | 2 |
ABCDABD | A, AB, ABC, ABCD, ABCDA, ABCDAB | D, BD, ABD, DABD, CDABD, BCDABD | 0 |
"部分匹配"的實質是,有時候,字符串頭部和尾部會有重複。好比,"ABCDAB"之中有兩個"AB",那麼它的"部分匹配值"就是2("AB"的長度)。搜索字符串移動的時候,第一個"AB"向後移動4位(字符串長度-部分匹配值),就能夠來到第二個"AB"的位置。
在KMP算法中有個數組,叫作前綴數組,也有的叫next數組,每個子串有一個固定的next數組,它記錄着字符串匹配過程當中失配狀況下能夠向前多跳幾個字符,固然它描述的也是子串的對稱程度,程度越高,值越大,固然以前可能出現再匹配的機會就更大。next數組的求法是KMP算法的關鍵,可是理解next數組並非一件輕鬆的事情。
由上文,咱們已經知道,字符串「ABCDABD」各個前綴後綴的最大公共元素長度分別爲:
並且,根據這個表能夠得出下述結論
把next 數組跟以前求得的最大長度表對比後,不難發現,next 數組至關於「最大長度值」 總體向右移動一位,而後初始值賦爲-1。意識到了這一點,你會驚呼原來next 數組的求解居然如此簡單!
換言之,對於給定的模式串:ABCDABD,它的最大長度表及next 數組分別以下:
根據最大長度表求出了next 數組後,從而有
右移位數 = 失配字符所在位置 - 失配字符對應的next 值
然後,你會發現,不管是基於《最大長度表》的匹配,仍是基於next 數組的匹配,二者得出來的向右移動的位數是同樣的。
接下來,我們來寫代碼求下next 數組。
基於以前的理解,可知計算next函數的方法能夠採用遞推,若是對於值k,有p0 p1, ..., pk-1 = pj-k pj-k+1, ..., pj-1,至關於next[j-1] = k。那麼對於pattern的前j 個序列字符,得
求next數組以下:
1 void getNext(const char *pattern, int *next, int pattern_len) 2 { 3 int i = 0; 4 int j = -1; 5 next[0] = -1; 6 7 while (i < pattern_len - 1) 8 { 9 10 if (j == -1 || pattern[i] == pattern[j]) 11 { 12 ++i; 13 ++j; 14 if (pattern[i] != pattern[j]) //正常狀況 15 next[i] = j; 16 else //特殊狀況,這裏即爲優化之處。考慮下AAAAB, 防止4個A造成012在匹配時屢次迭代。 17 next[i] = next[j]; 18 } 19 else 20 { 21 j = next[j]; 22 } 23 }
完整代碼以下:
1 #include <stdio.h> 2 #include <stdlib.h> 3 #include <string.h> 4 5 6 static inline void getNext(const char *pattern, int *next, int pattern_len) 7 { 8 int i = 0; 9 int j = -1; 10 next[0] = -1; 11 12 while (i < pattern_len - 1) 13 { 14 15 if (j == -1 || pattern[i] == pattern[j]) 16 { 17 ++i; 18 ++j; 19 if (pattern[i] != pattern[j]) //正常狀況 20 next[i] = j; 21 else //特殊狀況,這裏即爲優化之處。考慮下aaaab, 防止4個a造成012在匹配時屢次迭代。 22 next[i] = next[j]; 23 } 24 else 25 { 26 j = next[j]; 27 } 28 } 29 } 30 31 static inline bool match(const char *src, const char *pattern) 32 { 33 bool is_match = true; 34 35 int src_index = 0; 36 int pattern_index = 0; 37 int src_len = strlen(src); 38 int pattern_len = strlen(pattern); 39 40 //建立next數組,並初始化 41 int *next = (int *)malloc(pattern_len * sizeof(int)); 42 getNext(pattern, next, pattern_len); 43 44 //匹配主循環體 45 while (pattern_index < pattern_len && src_index < src_len) 46 { 47 //若對應位置字符匹配則右移1位,不然移動pattern 48 if (pattern_index == -1 || src[src_index] == pattern[pattern_index]) 49 { 50 src_index++; 51 pattern_index++; 52 } 53 else 54 { 55 pattern_index = next[pattern_index]; 56 } 57 } 58 59 //若pattern_index未達到串尾,代表pattern未完成匹配。不然便是完成匹配 60 if (pattern_index >= pattern_len) 61 { 62 is_match = true; 63 } 64 else 65 { 66 is_match = false; 67 } 68 69 return is_match; 70 } 71 72 73 int main(void) 74 { 75 char src[] = "aaaabacdeg"; 76 char pattern[] = "aabacd"; 77 78 bool res = match(src, pattern); 79 printf("res: %d\n", (int)res); 80 81 return 0; 82 }
本文有至關分量的內容參考借鑑了網絡上各位網友的熱心分享,特別是一些帶有徹底參考的文章,其後附帶的連接內容更直接、更豐富,筆者只是作了一下概括&轉述,在此一併表示感謝。