字符串匹配的kmp算法 及 python實現

一:背景

給定一個主串(以 S 代替)和模式串(以 P 代替),要求找出 P 在 S 中出現的位置,此即串的模式匹配問題。html

Knuth-Morris-Pratt 算法(簡稱 KMP)是解決這一問題的經常使用算法之一,這個算法是由高德納(Donald Ervin Knuth)和沃恩 · 普拉特在 1974 年構思,同年詹姆斯 ·H· 莫里斯也獨立地設計出該算法,最終三人於 1977 年聯合發表。python

在繼續下面的內容以前,有必要在這裏介紹下兩個概念:真前綴 和 真後綴算法

由上圖所得, "真前綴" 指除了自身之外,一個字符串的所有頭部組合;"真後綴" 指除了自身之外,一個字符串的所有尾部組合。數組

二:樸素字符串匹配算法

初遇串的模式匹配問題,咱們腦海中的第一反應,就是樸素字符串匹配(即所謂的暴力匹配)app

暴力匹配的時間複雜度爲 O(nm),其中 n 爲 S 的長度,m 爲 P 的長度。很明顯,這樣的時間複雜度很難知足咱們的需求。post

接下來進入正題:時間複雜度爲 Θ(n+m) 的 KMP 算法。spa

三:KMP 字符串匹配算法

3.1 算法流程

如下摘自阮一峯的字符串匹配的 KMP 算法,並做稍微修改。.net

(1)設計

首先,主串 "BBC ABCDAB ABCDABCDABDE" 的第一個字符與模式串 "ABCDABD" 的第一個字符,進行比較。由於 B 與 A 不匹配,因此模式串後移一位。3d

(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
  1. i = 0,對於模式串的首字符,咱們統一爲next[0] = -1
  2. i = 1,前面的字符串爲A,其最長相同真先後綴長度爲 0,即next[1] = 0
  3. i = 2,前面的字符串爲AB,其最長相同真先後綴長度爲 0,即next[2] = 0
  4. i = 3,前面的字符串爲ABC,其最長相同真先後綴長度爲 0,即next[3] = 0
  5. i = 4,前面的字符串爲ABCD,其最長相同真先後綴長度爲 0,即next[4] = 0
  6. i = 5,前面的字符串爲ABCDA,其最長相同真先後綴爲A,即next[5] = 1
  7. i = 6,前面的字符串爲ABCDAB,其最長相同真先後綴爲AB,即next[6] = 2
  8. 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     

KMP 算法精解及其 Python 版的代碼示例

相關文章
相關標籤/搜索