字符串查找以及KMP算法


字符串查找和匹配是一個很常用的功能,比如在爬蟲,郵件過濾,文本檢索和處理方面經常用到。相對與Cpython在字符串的查找方面有很多內置的庫可以供我們使用,省去了很多代碼工作量。但是我們還是需要了解一些常用的字符串查找算法的實現原理。

首先來看python內置的查找方法。查找方法有find,index,rindex,rfind方法。這里只介紹下find方法。find方法返回的是子串出現的首位置。比如下面的這個,返回的是abcstr中的首位置也就是3。如果沒找到將會返回-1

str = "dkjabcfkdfjkd198983abcdeefg"

print str.find('abc')

但是在str中有2abc,那么如何去查找到第二個abc的位置呢。由於find是會返回查找到的字符串的首位置,因此我們可以利用這個位置繼續往后搜索。代碼如下

def str_search_internal():

    str = "dkjabcfkdfjkd198983abcdeefg"

    substr='abc'

    substr_len=len(substr)

    start=0

    while start <=len(str):

        index=str.find('abc',start)

        if index == -1:

            return -1

        else:

            print index

            begin=index+substr_len #每一次查找后就將開始查找的位置往后移動字串的長度

            if begin <= len(str):

                index=str.find('abc',begin)

                print index

            else:

                return -1

            start=index+substr_len

通過返回的index方式就可以不斷的往下尋找后面匹配的字符串。

前面介紹了python內置的查找函數,那么如果我們不用這些內置的函數,自己如何編寫查找函數呢。首先我們來看下朴素的串匹配應用。代碼如下

def str_search():

    str = "dkjabcfkdfjkd198983abcdeefg"

    substr = 'abc'

    substr_len = len(substr)

    str_len=len(str)

    i,j=0,0

    while i < str_len and j < substr_len:  

        if str[i] ==  substr[j]: #如果匹配,則主串和子串的位置同時后移一位

            i+=1

            j+=1

        else:#如果不匹配,主串移動到當前查找位置的后一位

            i=i-j+1

            j=0

        if j == substr_len:  #如果j到了子串的最后一位,表明已經找到匹配的

            return i-j

    return -1

 

從代碼量的角度來看,直接使用find方法要省事得多。那么從代碼的運行時間來看,誰更快呢。來比較下代碼的運行時間

start=time.time()

str = "dkjabcfkdfjkd198983abcdeefg"

print str.find('abc')

end=time.time()

print 'the time elapsed %f' % (end-start)

find方法使用的時間為0.000009

/usr/bin/python2.7 /home/zhf/py_prj/data_struct/chapter4.py

3

the time elapsed 0.000009

用朴素的匹配算法運行時間是0.000026,時間是find方法的3

start=time.time()

ret=str_search()

print ret

end=time.time()

print 'the time elapsed %f' % (end-start)

/usr/bin/python2.7 /home/zhf/py_prj/data_struct/chapter4.py

3

the time elapsed 0.000026

 

我們考慮更極端的場景,將主串str初始化為

"dkjueireijkab139u8khbbzkjdfjdiuhfhhionknl90089122jjkdnbdfdfdfddfd981298989dhfjdbfjdbfjdbfjbj" \

          "djkjdfkdjkfbkadfffffffffffffffffffffffffffffffffffjiiernkenknkdfndkfndkfbdhfkdfjkd198983abcdeefg"

長度更長且abc位於主串的偏后的位置。此時再來比較下兩種方式的運行時間。

使用find方法的時間為0.000028

/usr/bin/python2.7 /home/zhf/py_prj/data_struct/chapter4.py

180

the time elapsed 0.000028

使用朴素查找方式的時間為0.000222. 時間增長了將近10倍。因為可以看到字符串的規模越大,使用find方法的時間越少,查找速度越快。

/usr/bin/python2.7 /home/zhf/py_prj/data_struct/chapter4.py

180

the time elapsed 0.000222

 

通過對比可以發現朴素查找算法的效率很低,原因在於執行的過程在不斷的回溯,匹配中遇到字符不同的時候,主串str將右移一個字符位置,隨后的匹配回到子串substr的開始繼續開始匹配。這種方式完全沒有利用之前查找得到的信息來進行下一步的判斷,可以認為是一種暴力搜索。假設有如下的場景主串是000000000000000000001。子串是001。而001位於最后3位。按照朴素查找方法,這個算法的復雜性為O(m*n).

 

由於朴素查找算法的效率太低,因此來看下一種查找效率更高的算法:KMP算法。

比如abc這個串找它在abababbcabcac這個串中第一次出現的位置,那么如果直接暴力雙重循環的話,abcc不匹配主串中的第三個字母a后,abc將開始從母串中的頭位置的下一個位置開始尋找,就是abc開始匹配主串中第二個字母bab...這樣。但是其實在主串中能夠匹配的字母應該是a開始的,因此abc去匹配bab一開始也是不匹配的。這樣多出了一次無謂的匹配。效率更高的方法就是用abc去匹配主串中的第三個字母aba

KMP的優勢就在於可以讓模式串向右滑動盡可能多的距離就是abc直接從模式串的第三個字母aba...開始匹配,為了實現這一目標,KMP需要預處理出模式串的移位跳轉next數組。

我們來用一個實際的例子來看下該如何優化

下面的例子在子串和主串比較到D的時候發現不一致了

按照朴素的搜索算法,將搜索詞整個后移一位,再從頭逐個比較。這樣做雖然可行,但是效率很差,因為你要把"搜索位置"移到已經比較過的位置,重比一遍

 

當空格與D不匹配時,你其實知道前面六個字符是"ABCDAB"KMP算法的想法是,設法利用這個已知信息,不要把"搜索位置"移回已經比較過的位置,繼續把它向后移,這樣就提高了效率。如下圖,因為有相同的AB存在,因此直接移動AB的位置進行比較

下面來介紹kmp算法的思想。

在字符串S中尋找M,假設匹配到位置i時兩個字符才出現不相等(i位置前的字符都相等),這時我們需要將字符串M向右移動。常規方法是每次向右移動一位,但是它沒有考慮前i-1位已經比較過這個事實,所以效率不高。事實上,如果我們提前計算某些信息,就有可能一次右移多位。假設我們根據已經獲得的信息知道可以右移x位,我們分析移位前后的M的特點,可以得到如下的結論:

B段字符串是M的一個前綴

A段字符串是M的一個后綴

A段字符串和B段字符串相等

所以右移x位之后,使M[k] 與 S[i] 對齊,繼續從i位置進行比較的前提是:M的前綴B和后綴A相同。

這樣就可以得出KMP算法的核心思想:

KMP算法的核心即是計算字符串M每一個位置之前的字符串的前綴和后綴公共部分的最大長度。獲得M每一個位置的最大公共長度之后,就可以利用該最大公共長度快速和字符串S比較。當每次比較到兩個字符串的字符不同時,我們就可以根據最大公共長度將字符串M向右移動,接着繼續比較下一個位置。

所以KMP就需要找出前綴和后綴的最大長度並記錄下來,也就是我們用到的next數組。

 

這里需要對前綴和后綴的意義進行解釋下

"前綴"指除了最后一個字符以外,一個字符串的全部頭部組合;"后綴"指除了第一個字符以外,一個字符串的全部尾部組合。舉個例子來說明下,比如字符串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

 

 

 

對於目標字符串ptrababaca,長度是7,所以next[0]next[1]next[2]next[3]next[4]next[5]next[6]分別計算的是 
aababaababababaababacababaca的相同的最長前綴和最長后綴的長度.所以next數組的值是[-1,-1,0,1,2,-1,0],這里-1表示不存在,0表示存在長度為0,2表示存在長度為2

 

 

為方便起見,next數組下標都從1開始,M數組下標依然從0開始

假設我們現在已經求得next[1]next[2]……next[i],分別表示不同長度子串的前綴和后綴最大公共長度,下面介紹如何用歸納法計算next[i+1] ?

假設knext[i],它表示M位置i之前的字符串的前綴和后綴最大公共長度,即M[0...k-1] = M[i-k...i-1]

比如假設子串為如下。

a  b  c  e   r  e  j  k  a  b  c   k

0  1  2  3  4  5  6  7  8  9  10 11

那么next[11]=3M[0-2]=M[11-3..11-1]=M[8,10]。這個就表示位置11之前的字符串的前綴和后綴的最大公共長度為3

接下來看下如何求出next數組. k表示前綴數據,i表示后綴數據

(1)若M[k] = M[i]相等則必定有next[i+1]  next[i] 1,即M位置i1之前的字符串的前綴和后綴最大公共長度為k+1。還是按照上面的字符串為例M[2]=M[10], next[11]=3,next[10]=2, 因此next[i+1]=next[i]+1

(2)若M[k] != M[i], 則 k索引next[k]直到與p[j]相等或者前綴子符串長度為0,此時可用公式next[j+1]=k+1

歸納一下:令 next[k] = j, 繼續判斷M[i] 與 M[j] 是否相等,如果相等則 next[i+1]  next[k] +  next[ next[i] ] + 1;如果不相等重復步驟(2),繼續分割長度為next[ next[k] ]的字符串,直到字符串長度為0為止

 

那么為了實現這種生成關系,我們需要設定一個標量,設定k的前綴初始值為-1next數組全部也初始為-1。我們還是以這個字符串為例.j=0

a  b  c  e   r  e  j  k  a  b  c   k

0  1  2  3  4  5  6  7  8  9  10 11

1  next[0]從第一個字母開始,沒有可對比前綴和后綴,因此等於-1.這里我們用-1這個標量也是為了表示沒有找到匹配的前綴和后綴.  j=0 k=-1

2  next[1]: ab的最大公共長度,由於k=-1,沒有可匹配的前綴。因此next[1]=0, j=1,k=0

3  next[2]: abc的最大公共長度,此時P[1]!=P[0], 繼續向前最小子串查找k=next[k=0]=-1沒有找到可匹配的前綴,因此next[2]=0 j=2,k=0

4  next[3]: abce的最大公共長度。由於p[2]!=p[0], 因此繼續往前綴查找。也就是k=next[k]=next[0]=-1. 由於k=-1沒有找到可匹配的前綴,因此next[3]=0. 變量j=3,k=0

.

.

.

.

.

5 next[9],abcerejkab的最大公共長度. p[8]=p[0]。因此next[9]=next[8]+1=0+1=1    j=9,k=1

6 next[10],abcerejkabc的最大公共長度. p[9]=p[1]。因此next[10]=next[9]+1=1+1=2    變量:j=10,k=2

7 next[11],abcerejkabck的最大公共長度. p[10]=p[2]。因此next[10]=next[9]+1=2+1=3    變量:j=11,k=3

 

根據前面的描述就可以得出我們的代碼實現

def get_next(substring):

    lenth=len(substring)

    next_table=[-1]*lenth

    k=-1

    j=0

while j < lenth-1:

#substring[j]表示后綴,substring[k]表示前綴

        if k==-1 or substring[j]==substring[k]:

            j+=1

            k+=1

            next_table[j]=k

            print j,k,next_table

   #沒有找到匹配的,繼續往前最小子串進行查找

        else:

            k=next_table[k]

    return next_table

 

因此可以得到next表如下:

[-1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2, 3]

 

既然next表已經得到,我們就可以寫出對應的KMP算法實現了

 

 

def kmp_function(str,substring):

    len1=len(str)

    len2=len(substring)

    next_table=get_next(substring)

    print next_table

    k=-1

    i=0

    while i < len1:

        while(k>-1 and substring[k+1] != str[i]):

            k=next_table[k]

        if substring[k+1] == str[i]:

            k=k+1

        if k==len2-1:

            return i-len2+1

        i+=1

 

從這個實現可以看出KMP的實現復雜度為O(m+n),比朴素的查找算法的O(m*n)提高了不少.那么我們繼續分析下KMP算法是否有優化的空間呢來看下面的例子:

如果用之前的next 數組方法求模式串“abab”next 數組,可得其next 數組為-1 0 0 10 0 1 2整體右移一位,初值賦為-1),當它跟下圖中的文本串去匹配的時候,發現bc失配,於是模式串右移j - next[j] = 3 - 1 =2位。

右移2位后,b又跟c失配。事實上,因為在上一步的匹配中,已經得知p[3] = b,與s[3] = c失配,而右移兩位之后,讓p[ next[3] ] = p[1] = b 再跟s[3]匹配時,必然失配。問題出在哪呢?

問題出在不該出現p[j] = p[ next[j] ]。為什么呢?理由是:當p[j] != s[i] 時,下次匹配必然是p[ next [j]] s[i]匹配,如果p[j] = p[ next[j] ],必然導致后一步匹配失敗(因為p[j]已經跟s[i]失配,然后你還用跟p[j]等同的值p[next[j]]去跟s[i]匹配,很顯然,必然失配),所以不能允許p[j] = p[ next[j] ]。如果出現了p[j] = p[ next[j] ]咋辦呢?如果出現了,則需要再次遞歸,即令next[j] = next[ next[j] ]。總結即是:

如果a位字符與它的next(next[a])指向的b位字符相等(即p[a] == p[next[a]],a位的next值就指向b位的next值即(next[ next[a] ])。

那么代碼修改如下就可以了:

def get_next(substring):

    lenth=len(substring)

    next_table=[-1]*lenth

    k=-1

    j=0

    while j < lenth-1:

        if k==-1 or substring[j]==substring[k]:

            j+=1

            k+=1

#如果p[j]==p[next[j]],則繼續遞歸,直到p[j]!=p[next[j]]

            if substring[j] == substring[k]:

                next_table[j]=next_table[k]

            else:

                next_table=k

        else:

            k=next_table[k]

    return next_table

 


免責聲明!

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



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