一:背景
給定一個主串(以 S 代替)和模式串(以 P 代替),要求找出 P 在 S 中出現的位置,此即串的模式匹配問題。
Knuth-Morris-Pratt 算法(簡稱 KMP)是解決這一問題的常用算法之一,這個算法是由高德納(Donald Ervin Knuth)和沃恩 · 普拉特在 1974 年構思,同年詹姆斯 ·H· 莫里斯也獨立地設計出該算法,最終三人於 1977 年聯合發表。
在繼續下面的內容之前,有必要在這里介紹下兩個概念:真前綴 和 真后綴。

由上圖所得, "真前綴" 指除了自身以外,一個字符串的全部頭部組合;"真后綴" 指除了自身以外,一個字符串的全部尾部組合。
二:朴素字符串匹配算法
初遇串的模式匹配問題,我們腦海中的第一反應,就是朴素字符串匹配(即所謂的暴力匹配)
暴力匹配的時間復雜度為 O(nm),其中 n 為 S 的長度,m 為 P 的長度。很明顯,這樣的時間復雜度很難滿足我們的需求。
接下來進入正題:時間復雜度為 Θ(n+m) 的 KMP 算法。
三:KMP 字符串匹配算法
3.1 算法流程
以下摘自阮一峰的字符串匹配的 KMP 算法,並作稍微修改。
(1)

首先,主串 "BBC ABCDAB ABCDABCDABDE" 的第一個字符與模式串 "ABCDABD" 的第一個字符,進行比較。因為 B 與 A 不匹配,所以模式串后移一位。
(2)

因為 B 與 A 又不匹配,模式串再往后移。
(3)

就這樣,直到主串有一個字符,與模式串的第一個字符相同為止。
(4)

接着比較主串和模式串的下一個字符,還是相同。
(5)

直到主串有一個字符,與模式串對應的字符不相同為止。
(6)

這時,最自然的反應是,將模式串整個后移一位,再從頭逐個比較。這樣做雖然可行,但是效率很差,因為你要把 "搜索位置" 移到已經比較過的位置,重比一遍。
(7)

一個基本事實是,當空格與 D 不匹配時,你其實是已經知道前面六個字符是 "ABCDAB"。KMP 算法的想法是,設法利用這個已知信息,不要把 "搜索位置" 移回已經比較過的位置,而是繼續把它向后移,這樣就提高了效率。
(8)
| i | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
|---|---|---|---|---|---|---|---|---|
| 模式串 | A | B | C | D | A | B | D | '\0' |
| next[i] | -1 | 0 | 0 | 0 | 0 | 1 | 2 | 0 |
怎么做到這一點呢?可以針對模式串,設置一個跳轉數組int next[],這個數組是怎么計算出來的,后面再介紹,這里只要會用就可以了。
(9)

已知空格與 D 不匹配時,前面六個字符 "ABCDAB" 是匹配的。根據跳轉數組可知,不匹配處 D 的 next 值為 2,因此接下來從模式串下標為 2 的位置開始匹配。
(10)

因為空格與C不匹配,C 處的 next 值為 0,因此接下來模式串從下標為 0 處開始匹配。
(11)

因為空格與 A 不匹配,此處 next 值為 - 1,表示模式串的第一個字符就不匹配,那么直接往后移一位。
(12)

逐位比較,直到發現 C 與 D 不匹配。於是,下一步從下標為 2 的地方開始匹配。
(13)

逐位比較,直到模式串的最后一位,發現完全匹配,於是搜索完成。
3.2 next 數組是如何求出的展開目錄
next 數組的求解基於 “真前綴” 和 “真后綴”,即next[i]等於P[0]...P[i - 1]最長的相同真前后綴的長度(請暫時忽視 i 等於 0 時的情況,下面會有解釋)。我們依舊以上述的表格為例,為了方便閱讀,我復制在下方了。
| i | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
|---|---|---|---|---|---|---|---|---|
| 模式串 | A | B | C | D | A | B | D | '\0' |
| next[i] | -1 | 0 | 0 | 0 | 0 | 1 | 2 | 0 |
- i = 0,對於模式串的首字符,我們統一為
next[0] = -1; - i = 1,前面的字符串為
A,其最長相同真前后綴長度為 0,即next[1] = 0; - i = 2,前面的字符串為
AB,其最長相同真前后綴長度為 0,即next[2] = 0; - i = 3,前面的字符串為
ABC,其最長相同真前后綴長度為 0,即next[3] = 0; - i = 4,前面的字符串為
ABCD,其最長相同真前后綴長度為 0,即next[4] = 0; - i = 5,前面的字符串為
ABCDA,其最長相同真前后綴為A,即next[5] = 1; - i = 6,前面的字符串為
ABCDAB,其最長相同真前后綴為AB,即next[6] = 2; - i = 7,前面的字符串為
ABCDABD,其最長相同真前后綴長度為 0,即next[7] = 0。
那么,為什么根據最長相同真前后綴的長度就可以實現在不匹配情況下的跳轉呢?舉個代表性的例子:假如i = 6時不匹配,此時我們是知道其位置前的字符串為ABCDAB,仔細觀察這個字符串,首尾都有一個AB,既然在i = 6處的 D 不匹配,我們為何不直接把i = 2處的 C 拿過來繼續比較呢,因為都有一個AB啊,而這個AB就是ABCDAB的最長相同真前后綴,其長度 2 正好是跳轉的下標位置。
python實現,如下:
def partial_table(p): '''''partial_table("ABCDABD") -> [0, 0, 0, 0, 1, 2, 0]''' prefix = set() postfix = set() ret = [0] for i in range(1, len(p)): prefix.add(p[:i]) postfix = {p[j:i + 1] for j in range(1, i + 1)} ret.append(len((prefix & postfix or {''}).pop())) return ret print partial_table("ABCDABD") #[0, 0, 0, 0, 1, 2, 0]
全部代碼:
#coding=utf-8 def kmp_match(s, p): m = len(s); n = len(p) cur = 0 # 起始指針cur table = partial_table(p) while cur <= m - n: #只去匹配前m-n個 for i in range(n): if s[i + cur] != p[i]: cur += max(i - table[i - 1], 1) # 有了部分匹配表,我們不只是單純的1位1位往右移,可以一次移動多位 break else: #for 循環中,如果沒有從任何一個 break 中退出,則會執行和 for 對應的 else #只要從 break 中退出了,則 else 部分不執行。 return True return False # 部分匹配表 def partial_table(p): '''''partial_table("ABCDABD") -> [0, 0, 0, 0, 1, 2, 0]''' prefix = set() postfix = set() ret = [0] for i in range(1, len(p)): prefix.add(p[:i]) postfix = {p[j:i + 1] for j in range(1, i + 1)} ret.append(len((prefix & postfix or {''}).pop())) return ret print partial_table1("ABCDABD") print kmp_match("BBC ABCDAB ABCDABCDABDE", "ABCDABD")
參考 如何理解 KMP
