kmp算法簡明教程


在字符串s中尋找模式串p的位置,這是一個字符串匹配問題。

舉例說明:

 i = 0   1   2   3   4   5   6  7   8   9  10  11  12  13
 s = a   b   a   a   c   a   b  a   a   a   b   a   a   b
 p = a   b   a   a   b
 j = 0   1   2   3   4

在kmp算法發明之前。人們使用這種算法:

'''
原始的求子串位置算法,O( m * n )
'''
def string_index_of( pstr, pattern, pos = 0 ):
    str_index = pos
    pattern_index = 0
    pattern_len = len( pattern )
    while str_index < len( pstr ) and pattern_index < pattern_len:
        if pstr[ str_index ] == pattern[ pattern_index ]:
            str_index += 1
            pattern_index += 1
        else:
            str_index = str_index - pattern_index + 1
            pattern_index = 0
    if pattern_index == pattern_len:
        return str_index - pattern_index
    return -1

pstr = 'i am caochao, i love coding!'
pattern = 'ao'
print( string_index_of( pstr, pattern, 7 ) )
print( pstr.find( pattern ) )

當s[4]與p[4]失配時,主串s回溯到i=1,模式串回溯到j=0,然后從此位置繼續匹配。顯然這種做法效率低下,假設s長度為n,p長度為m,則其時間復雜度為O(m*n)。

現考慮這樣一個問題,當s與p匹配到位置i,j處,s[j]不等於p[j],如果此時保持i不變,p串中從k(0<k<j)處繼續匹配,這樣無需回溯i的做法這就是我們要講到的kmp算法。那么應當如何取得這個k值?可以預先求出p中每個失配的j處需要跳轉到的位置k(next[j]),這就是kmp算法中的next數組。

kmp算法步驟如下:

1,初始化i,j均為0,

2,依次往后比較s[i]與p[j],若相等則i,j各自加1,否則保持i不變,j=k(next[j])。若某時刻求得j值為-1,i,j也各自加1然后繼續匹配

3,重復步驟2

依據上述分析可知,kmp算法中最關鍵之處在於k值的取法。在匹配進行到s[i]不等於p[j]時,假設應當讓s[i]與p[k]繼續比較(0<k<j),那么p中前k個字符必須滿足,且不能存在k'>k滿足等式1:

等式1,p[0,k-1]=s[i-k,i-1]

而在i,j之前已經匹配的字符里存在等式2:

等式2,p[j-k,j-1]=s[i-k,i-1]

由此,可以推出等式3:

等式3,p[0,k-1]=p[j-k,j-1]

至此,k值的取法已經非常明顯,即在p串中取最大的k(0<k<j),使得p中開始的k個字符與p[j]之前的k個字符相等。這樣就可在s[i]不等於p[j]時,盡可能在距離p[0]遠的位置處繼續匹配,從而提高匹配效率。

從上面的分析中可以給出k,即next[j]的定義:

1,j=0時,next[j]=-1

2,next[j] = max{k|0<k<j且p[0,k-1]=p[j-k,j-1]}

3,其它情況,next[j]=1

遞推求next數組:

從next[j]的定義出發,可以采用遞推的方式求得next[j]:

首先,next[0]=-1

令next[j]=k(0<k<j),則表明在p中存在k,且不存在k'>k滿足下列關系:

p[0,k-1]=p[j-k,j-1]

那么next[j+1]的取值有3種情況,

1,若p[k]=p[j],則表明在p中存k,且不存在k'>k滿足關系p[0,k]=p[j-k,j],那么next[j+1]=k+1,即

next[j+1]=next[j]+1

2,若p[k]不等於p[j],此時可把求next函數的過程看成模式匹配的過程,即p既是主串又是模式串。而在模式匹配過種中,此時應當讓p[j]與p[next[k]]繼續比較。

為了便於理解,這里令next[k]=k'。

若p[j]=p[k']時,next[j+1]=k'+1,即next[j+1]=next[k]+1,也即

next[j+1]=next[next[j]]+1

同理若p[j]不等於p[k'],那么繼續讓p[j]與next[k']比較,依次類推,直至k'=-1時:

next[j+1]=0

代碼實現如下:

'''
kmp求next[j]數組
'''
def kmp_get_next( pattern ):
    i = 0
    j = -1
    _next = [ 0 ] * len( pattern )
    _next[ 0 ] = -1
    while i < len( pattern ) - 1:
        if j == -1 or pattern[ i ] == pattern[ j ]:
            i += 1
            j += 1
            _next[ i ] = j
        else:
            j = _next[ j ]
    return _next

優化的求next數組方法:

考慮如下模式串:

j       =   0    1    2    3    4
p       =   a    a    a    a    b
next[j] =  -1    0    1    2    3

若某時刻s[i]與p[3]不相等,依據next[3]指示應當讓s[i]與p[2]繼續比較,因p[3]與p[2]相等,這一步明顯是多余的。推廣到普遍情況,在求next數組過程中,如果next[i]=j且p[i]=p[j],那么令next[i]=next[j]。代碼如下:

'''
kmp求next[j]數組
'''
def kmp_get_next( pattern ):
    i = 0
    j = -1
    _next = [ 0 ] * len( pattern )
    _next[ 0 ] = -1
    while i < len( pattern ) - 1:
        if j == -1 or pattern[ i ] == pattern[ j ]:
            i += 1
            j += 1
            if pattern[ i ] == pattern[ j ]:
                _next[ i ] = _next[ j ]
            else:
                _next[ i ] = j
        else:
            j = _next[ j ]
    return _next

預先求得next[j]數組后,kmp算法代碼實現如下:

'''
kmp求子串位置
'''
def kmp_index_of( pstr, pattern, pos = 0 ):
    _next = kmp_get_next( pattern )
    str_index = pos
    pattern_index = 0
    pattern_len = len( pattern )
    while str_index < len( pstr ) and pattern_index < pattern_len:
        if pattern_index == -1 or pstr[ str_index ] == pattern[ pattern_index ]:
            str_index += 1
            pattern_index += 1
        else:
            pattern_index = _next[ pattern_index ]
    if pattern_index == pattern_len:
        return str_index - pattern_index
    return -1

pstr = 'i am caochao, i love coding!'
pattern = 'ao'
print( kmp_index_of( pstr, pattern, 7 ) )
print( pstr.find( pattern ) )

最后,對比下普通算法與kmp算法解決本文開始提出的問題匹配過程:

普通算法:

 i = 0   1   2   3   4   5   6   7   8   9  10  11  12  13
 s = a b a a c a b a a a b a a b p = a b a a b j = 0 1 2 3 4

s[4]不等於p[4],令i=1,j=0

 i = 0   1   2   3   4   5   6   7   8   9  10  11  12  13
 s = a   b   a   a   c   a   b   a   a   a   b   a   a   b
 p =     a   b   a   a   b
 j =     0   1   2   3   4

s[1]不等於p[0],令i=2,j=0

 i = 0   1   2   3   4   5   6   7   8   9  10  11  12  13
 s = a   b   a   a   c   a   b   a   a   a   b   a   a   b
 p =         a   b   a   a   b
 j =         0   1   2   3   4

s[3]不等於p[1],令i=3,j=0

 i = 0   1   2   3   4   5   6   7   8   9  10  11  12  13
 s = a   b   a   a   c   a   b   a   a   a   b   a   a   b
 p =             a   b   a   a   b
 j =             0   1   2   3   4

限於篇幅,略過中間n步,跳至i=9,j=0

 i = 0   1   2   3   4   5   6   7   8   9  10  11  12  13
 s = a   b   a   a   c   a   b   a   a   a   b   a   a   b
 p =                                     a   b   a   a   b
 j =                                     0   1   2   3   4

一路i++,j++,直到i=14,跳出循環結束匹配,並返回9。

kmp算法:

p串next數組為:

j       =   0    1    2    3    4
p       = a b a a b next[j] = -1 0 0 1 1

next數組優化過后變為:

j       =   0    1    2    3    4
p       =   a    b    a    a    b
next[j] =  -1    0   -1    1    0

下面開始匹配:

 i = 0   1   2   3   4   5   6   7   8   9  10  11  12  13
 s = a   b   a   a   c   a   b   a   a   a   b   a   a   b
 p = a   b   a   a   b
 j = 0   1   2   3   4

s[4]不等於p[4],令j=0

 i = 0   1   2   3   4   5   6   7   8   9  10  11  12  13
 s = a   b   a   a   c   a   b   a   a   a   b   a   a   b
 p =                 a   b   a   a   b
 j =                 0   1   2   3   4

s[4]不等於p[0],next[0]=-1,因此i,j各自加1。i=5,j=0

 i = 0   1   2   3   4   5   6   7   8   9  10  11  12  13
 s = a   b   a   a   c   a   b   a   a   a   b   a   a   b
 p =                     a   b   a   a   b
 j =                     0   1   2   3   4

i++,j++,直到s[9]不等於p[4],令j=0

 i = 0   1   2   3   4   5   6   7   8   9  10  11  12  13
 s = a   b   a   a   c   a   b   a   a   a   b   a   a   b
 p =                                     a   b   a   a   b
 j =                                     0   1   2   3   4

一路i++,j++,直到i=14,跳出循環結束匹配,並返回9。

通過對比可以看出,kmp算法比普通算法快得多,只要預先求出模式串next數組,則整個匹配過程中i無需回溯,時間復雜度亦由普通算法O(m*n)提升為O(m+n)。


免責聲明!

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



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