kmp演算法(上)

來源:互聯網
上載者:User

原創文章,轉載請註明出處:http://blog.csdn.net/fastsort/article/details/9903153 

1、字串匹配

所謂字串匹配,就是在主串S中尋找模式串P,如果存在,則返回P在S中的起始下標,否則返回不存在資訊(這裡用-1表示)。例如:

S=abcdabce,T=cda,

則返回2,如果T=cdd,則返回-1.

考慮C/C++中字串下標都是從0開始,以下討論都是在字串下標從0開始的前提下進行【有特殊說明的除外】。

 

2、常規演算法

       在確定找到高效演算法前,“暴力”演算法(brute force)雖然效率差強人意,但是總是值得信任的。

       字串匹配演算法也是這樣。直接匹配也很簡單,把主串S的第一個字元和模式串P的第一個字元對齊,然後兩個串同時向後比較:

                如果一直到P的結尾都匹配,很好,匹配成功,返回0;

                如果中間存在不匹配的字元,那麼把S的第二個字元和P的第一個字元對齊,然後兩個串同時向後比較,直到繼續S的第三個字元或者是匹配成功返回1……

                重複上述過程,直到S的結尾,這時返回-1,說明匹配失敗。

       代碼如下:

int match_1(const char * s, const char *p){    int slen=strlen(s),plen=strlen(p);    int i=0,j=0;    while(i<slen)    {        while(j<plen &&(i+j)<slen && s[i+j]==p[j]) j++;        if(j==plen) return i;        i++;        j=0;    }    return -1;}

這段代碼運行良好,但是為了對比,將其改為如下形式:

int    match_2(const char * s, const char *p){    int slen=strlen(s),plen=strlen(p);    int i=0,j=0;    while(i<slen && j<plen)    {        if(s[i]==p[j])///匹配        {            i++;            j++;        }        else///不匹配了        {            i = i-j+1;///回退S的指標            j = 0;///P從頭開始        }    }    if(j==plen)   return i-j;///匹配成功    else       return -1;}

簡單的說,就是i,j初始時分別指向s和p的起始位置,匹配時i,j同時向後移動,失配時i指標回退,j指標置零。

對於以上代碼,你應當完全理解,給你筆和紙你就能準確無誤的立刻寫出來。這樣你就對其工作過程了如指掌,否則後面的內容將難於理解。

 

複雜度分析:在最壞情況下,p每次都匹配到倒數第二個字元,然後最後一個字元不匹配,這時從頭再來,顯然每次p匹配都需要O(m)(其中m=strlen(p)),對s中的每個字元都匹配一遍,也需要O(n)(其中n=strlen(s)),總的複雜度就是O(mn)。

例如s=0000000000001,p=00001時,m=strlen(p)=5,n=strlen(s)=13.對於i∈[0-12],j∈[0-4],每次匹配到p[4]時,發現s[i]!=p[j]不匹配,都需要從頭開始(j=0,i=i-j+1)下次匹配,即所謂的“回溯”。

我們發現,在這個過程中其實有很多回溯過程是沒有必要的。比如:

i      0123456

s     0000000000001

p     00001

j      01234

在發現s[4]!=p[4]時,下次比較將從s[1]和p[0]比較開始:

i      0123456

s     0000000000001

p       00001

j        01234

一直比較到s[5]!=p[4]。其實如果注意到,在上一輪比較中,s[4]!=p[4]時(i=4,j=4),已經有

s[0...3]=p[0...3]    
  ①

所以有

s[1...3]=p[1...3]   
  ②

p[1...3]=p[0...2] 
           
 ③

③中,p[0...2]為p的首碼,p[1...3]為尾碼,這是由p本身的性質決定的,由②和③可以得到

s[1...3]=p[0...2] 
                             ④

看到這裡你有什麼感覺?既然s[1...3]=p[0...2]了,那為什麼下一輪匹配過程還要從s[1]和p[0]開始呢?!直接比較s[4]和p[3](即s[i]和p[k],k=3)就可以了。kmp演算法就是基於這個原理的,即發生失配後不需要每次都從p串的起始位置開始新的一輪比較(即i指標回退、j指標置零)。

       上面這段分析,尤其是這三個等式,最好理解透徹。這裡沒有用各種ijk,而是用實際的字串和數字表示,就是為了便於理解。理解透徹之後,就開始正式講解kmp演算法了。

 

3、kmp演算法

       kmp演算法對一般字元串匹配演算法(即match_2)的改進之處在於:發生失配之後,i指標不動,j指標指向合適的位置,開始下一輪的匹配。

       假設合適的位置已經儲存在一個數組next[n]中,n=strlen(p)。那麼kmp的代碼就是:

int match_3(const char * s, const char *p)

{

    int slen=strlen(s),plen=strlen(p);

    int i=0,j=0;

    while(i<slen && j<plen)

    {

        if(s[i]==p[j])///匹配

        {

            i++;

            j++;

        }

        else///不匹配了

        {

            //i =i-j+1; i指標不變,不再回退

            j = next[j];///j指標從某個位置開始

        }

    }

    if(j==plen)   return i-j;///匹配成功

    else       return -1;

}

發生改變的地方就在else裡:i指標不再回退,j指標不是歸零而是某個特定的值。看上去是不是比樸素匹配演算法更簡單?貌似是的,但是問題是這個特定值是什嗎?也就是說這個next數組怎麼求呢?好,現在來說這個next數組。

 

       從第二部分的最後面的分析(就是①②③那三個式子那)我們知道,當s[i]!=p[j]時(失配了),已經匹配的部分為

s[i-j…i-1]=p[0…j-1]                                               ④

即s[i]和p[j]的前j-1個字元。

如果想從p的第k個字元開始下一輪的匹配而不是回溯,那麼必須要滿足:p的前k-1個字元和s[i]前面的k-1個字元匹配(即k=next[j])。

p的前k-1個字元為: p[0…k-1]

s[i]的前k-1個字元為:s[i-k…i-1]

也就是

p[0…k-1]=s[i-k…i-1]                                              ⑤

由④可得:

s[i-k…i-1]=p[j-k…j-1]                                             ⑥

即s[i-j…i-1]和後k-1個字元和p的後k-1個字元相等。

由⑤⑥得

p[0…k-1]=p[j-k…j-1]                                            ⑦

如果第一次看到這裡,可能就有點暈了。我們再回顧下我們的問題。在p[j]處發生失配(s[i]!=p[j])後,i不動,j不需要置零,而是從k=next[j]處取得,那麼這個k要滿足⑦。

⑦是什麼意思呢?左面是p的前k個字元(首碼),右面是p[0…j-1]的後k個字元(尾碼)。這個k越大越好,為什嗎?因為越大j向後滑動的越多,效率越高。但是必須有k<=j-1 => k<j.

為便於理解,舉個例子,對於p=”000011”:

j=0  ?

j=1  p[1]前有p[0]=p[0],                    next[1]=0

j=2  p[0-1]=p[0-1],                           next[2]=1

j=3  p[0-2]=p[0-2],                           next[3]=2

j=4  p[0-3]=p[0-3],                           next[4]=3

j=5  p[0…4]沒有首碼=尾碼,            next[5]=0


j=0時怎麼辦呢?考慮最原始的情況,s[i]!=p[0]了,第一個就不匹配,當然沒法跳了,開始下一輪即可。所以這裡可以設定一個特殊值,表示第一個(j=0)就不匹配,一般設定為-1,可以簡化程式。在j=-1時,需要進行下一輪匹配(i++;j++)j++後剛好是0。如果你一定要設定為-9,那麼你要判斷,當j==-9時,下一輪要:(i++; j=0;)。你還會注意到,對任何長度大於2的p串,next[1]==0是恒成立的。

 

再來一個例子:

p[]=”abababacd”的next數組next[9]:

j=0,k=next[0]=-1;

j=1,k=next[1]=0

j=2,p[2]之前有”ab”,其首碼只有’a’!=尾碼’b’,所以k=next[2]=0

j=3,p[3]前為”aba”,p[0]=p[2],                     k=next[3]=1

j=4,p[4]前為”abab”,p[0…1]=p[2…3], k=next[4]=2

j=5,p[5]前為ababa,p[0…2]=p[2…4],  k=next[5]=3

j=6,p[6]前為ababab,p[0…3]=p[2…5], k=next[6]=4

j=7,p[7]前為abababa,p[0…4]=p[2…6],k=next[7]=5

j=8,p[8]前為abababac,由於最後一個字母是c,任何首碼都沒有和其相等的尾碼,所以k=next[8]=0


相信通過這2個例子,你應該理解了next數組了吧。


由於next數組中存在j=-1的情況,所以我們的函數應該做少許修改:

intkmp(const char * s, const char *p){    int slen=strlen(s),plen=strlen(p);    int *next = (int*)malloc(sizeof(int)*plen);    getNext(p,next);    int i=0,j=0;    while(i<slen && j<plen)    {        if(j==-1|| s[i]==p[j])///j=-1時,也是進行下一輪匹配        {            i++;            j++;        }        else        {            j = next[j];///i,j不再回溯        }    }    free(next);    if(j==plen)  return i-j;    else       return -1;}

 

       我把函數名也修改為kmp,這也是kmp演算法的最終形式(未最佳化的,後面再說最佳化的情況)。總共不到10行。

      其中的getNext()函數就是計算next數組的,如果你理解了next數組的計算方法,你可以動手寫一下試試,其代碼也不過十行,而且和上面的kmp函數驚人的相似。至於getNext()的思路和具體代碼,還在留給下一篇blog吧。

原創文章,轉載請註明出處:http://blog.csdn.net/fastsort/article/details/9903153 

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.