天天看點

了解KMP

從頭到尾徹底了解KMP(2014年8月22日版)

KMP字元串模式比對通俗點說就是一種在一個字元串中定位另一個串的高效算法。簡單比對算法的時間複雜度為O(m*n),KMP比對算法,可以證明它的時間複雜度為O(m+n).。

一.簡單比對算法

先來看一個簡單比對算法的函數:

//簡單的字元串比對算法

#include <iostream>

using namespace std;

int Index_BF(char s[], char T[], int pos)
{
    int i = pos, j = 0;
    while (s[i+j]!='\0'&&T[j]!='\0')
    {
        if (s[i+j]==T[j])
        {
            j++;    //繼續比較後一字元
        }
        else
        {
            i++;
            j = 0;   //重新開始新一輪的比對
        }
    }
    if (T[j] == '\0')
    {
        return i; //比對成功傳回下标
    }
    else
        return -1; //串s中(第pos個字元起)不存在和串T相同的子串
}

int matchString(const string& target,const string& pattern)
{
    int target_size = target.size();
    int pattern_size = pattern.size();
    int target_index = 0;
    int pattern_index = 0;
    if (target_size<=0||pattern_size<=0||target_size<pattern_size)
    {
        return -1;
    }
    while (target_index<target_size&&pattern_index<pattern_size)//  不能有等于号
    {
        if (target[target_index]==pattern[pattern_index])
        {
            target_index++;
            pattern_index++;
        }
        else
        {
            target_index = target_index - pattern_index + 1;  //目前标志-比對的長度+1
            pattern_index = 0;
        }
    }
    if (pattern_index==pattern_size&&target_index<=target_size)
    {
        return target_index - pattern_size ;
    }
    else
    {
        return -1;
    }
}

int main()
{
    cout << "第一種方法:" << Index_BF("banananobano", "nano", 0) << endl;

    cout << "第二種方法:"<<matchString("banananobano", "nano")<<endl;

    return 0;
}      

此算法的思想是直截了當的:将主串S中某個位置i起始的子串和模式串T相比較。即從 j=0 起比較 S[i+j] 與 T[j],若相等,則在主串 S 中存在以 i 為起始位置比對成功的可能性,繼續往後比較( j逐漸增1 ),直至與T串中最後一個字元相等為止,否則改從S串的下一個字元起重新開始進行下一輪的"比對",即将串T向後滑動一位,即 i 增1,而 j 退回至0,重新開始新一輪的比對。

例如:在串S=”abcabcabdabba”中查找T=” abcabd”(我們可以假設從下标0開始):先是比較S[0]和T[0]是否相等,然後比較S[1] 和T[1]是否相等…我們發現一直比較到S[5] 和T[5]才不等。如圖:

當這樣一個失配發生時,T下标必須回溯到開始,S下标回溯的長度與T相同,然後S下标增1,然後再次比較。如圖:

這次立刻發生了失配,T下标又回溯到開始,S下标增1,然後再次比較。如圖:

又一次發生了失配,是以T下标又回溯到開始,S下标增1,然後再次比較。這次T中的所有字元都和S中相應的字元比對了。函數傳回T在S中的起始下标3。如圖:

二. KMP比對算法

還是相同的例子,在S=”abcabcabdabba”中查找T=”abcabd”,如果使用KMP比對算法,當第一次搜尋到S[5] 和T[5]不等後,S下标不是回溯到1,T下标也不是回溯到開始,而是根據T中T[5]==’d’的模式函數值(next[5]=2,為什麼?後面講),直接比較S[5] 和T[2]是否相等,因為相等,S和T的下标同時增加;因為又相等,S和T的下标又同時增加。。。最終在S中找到了T。如圖:

KMP比對算法和簡單比對算法效率比較,一個極端的例子是:

在S=“AAAAAA…AAB“(100個A)中查找T=”AAAAAAAAAB”, 簡單比對算法每次都是比較到T的結尾,發現字元不同,然後T的下标回溯到開始,S的下标也要回溯相同長度後增1,繼續比較。如果使用KMP比對算法,就不必回溯.

對于一般文稿中串的比對,簡單比對算法的時間複雜度可降為O (m+n),是以在多數的實際應用場合下被應用。

KMP算法的核心思想是利用已經得到的部分比對資訊來進行後面的比對過程。看前面的例子。為什麼T[5]==’d’的模式函數值等于2(next[5]=2),其實這個2表示T[5]==’d’的前面有2個字元和開始的兩個字元相同,且T[5]==’d’不等于開始的兩個字元之後的第三個字元(T[2]=’c’).如圖:

也就是說,如果開始的兩個字元之後的第三個字元也為’d’,那麼,盡管T[5]==’d’的前面有2個字元和開始的兩個字元相同,T[5]==’d’的模式函數值也不為2,而是為0。

前面我說:在S=”abcabcabdabba”中查找T=”abcabd”,如果使用KMP比對算法,當第一次搜尋到S[5]和T[5]不等後,S下标不是回溯到1,T下标也不是回溯到開始,而是根據T中T[5]==’d’的模式函數值,直接比較S[5]和T[2]是否相等。。。為什麼可以這樣?剛才又說:“(next[5]=2),其實這個2表示T[5]==’d’的前面有2個字元和開始的兩個字元相同”。請看圖 :因為,S[4] ==T[4],S[3] ==T[3],根據next[5]=2,有T[3]==T[0],T[4] ==T[1],是以S[3]==T[0],S[4] ==T[1](兩對相當于間接比較過了),是以,接下來比較S[5] 和T[2]是否相等。。。

有人可能會問:S[3]和T[0],S[4] 和T[1]是根據next[5]=2間接比較相等,那S[1]和T[0],S[2] 和T[0]之間又是怎麼跳過,可以不比較呢?因為S[0]=T[0],S[1]=T[1],S[2]=T[2],而T[0] != T[1], T[1] != T[2],==> S[0] != S[1],S[1] != S[2],是以S[1] != T[0],S[2] != T[0]. 還是從理論上間接比較了。

有人疑問又來了,你分析的是不是特殊輕況啊。

假設S不變,在S中搜尋T=“abaabd”呢?答:這種情況,當比較到S[2]和T[2]時,發現不等,就去看next[2]的值,next[2]=-1,意思是S[2]已經和T[0] 間接比較過了,不相等,接下來去比較S[3]和T[0]吧。

假設S不變,在S中搜尋T=“abbabd”呢?答:這種情況當比較到S[2]和T[2]時,發現不等,就去看next[2]的值,next[2]=0,意思是S[2]已經和T[2]比較過了,不相等,接下來去比較S[2]和T[0]吧。

假設S=”abaabcabdabba”在S中搜尋T=“abaabd”呢?答:這種情況當比較到S[5]和T[5]時,發現不等,就去看next[5]的值,next[5]=2,意思是前面的比較過了,其中,S[5]的前面有兩個字元和T的開始兩個相等,接下來去比較S[5]和T[2]吧。

總之,有了串的next值,一切搞定。那麼,怎麼求串的模式函數值next[n]呢?(本文中next值、模式函數值、模式值是一個意思。)

三. 怎麼求串的模式值next[n]

定義:

(1)next[0]= -1 意義:任何串的第一個字元的模式值規定為-1。

(2)next[j]= -1 意義:模式串T中下标為j的字元,如果與首字元相同,且j的前面的1—k個字元與開頭的1—k個字元不等(或者相等但T[k]==T[j])(1≤k<j)。如:T=”abCabCad” 則 next[6]=-1,因T[3]=T[6]

(3)next[j]=k 意義:模式串T中下标為j的字元,如果j的前面k個字元與開頭的k個字元相等,且T[j] != T[k](1≤k<j)。即T[0]T[1]T[2]。。。T[k-1]==T[j-k]T[j-k+1]T[j-k+2]…T[j-1]且T[j] != T[k].(1≤k<j);

(4) next[j]=0 意義:除(1)(2)(3)的其他情況。

舉例:

01)求T=“abcac”的模式函數的值。

next[0]= -1 根據(1)

next[1]=0 根據 (4) 因(3)有1<=k<j;不能說,j=1,T[j-1]==T[0]

next[2]=0 根據 (4) 因(3)有1<=k<j;(T[0]=a)!=(T[1]=b)

next[3]= -1 根據 (2)

next[4]=1 根據 (3) T[0]=T[3] 且 T[1]=T[4]

下标 1 2 3 4
T a b c
next -1

若T=“abcab”将是這樣:

為什麼T[0]==T[3],還會有next[4]=0呢, 因為T[1]==T[4], 根據 (3)” 且T[j] != T[k]”被劃入(4)。

02)來個複雜點的,求T=”ababcaabc” 的模式函數的值。

next[1]=0 根據(4)

next[2]=-1 根據 (2)

next[3]=0 根據 (3) 雖T[0]=T[2] 但T[1]=T[3] 被劃入(4)

next[4]=2 根據 (3) T[0]T[1]=T[2]T[3] 且T[2] !=T[4]

next[5]=-1 根據 (2)

next[6]=1 根據 (3) T[0]=T[5] 且T[1]!=T[6]

next[7]=0 根據 (3) 雖T[0]=T[6] 但T[1]=T[7] 被劃入(4)

next[8]=2 根據 (3) T[0]T[1]=T[6]T[7] 且T[2] !=T[8]

5 6 7 8

隻要了解了next[3]=0,而不是=1,next[6]=1,而不是= -1,next[8]=2,而不是= 0,其他的好象都容易了解。

03) 來個特殊的,求 T=”abCabCad” 的模式函數的值。

C d

next[5]= 0 根據 (3) 雖T[0]T[1]=T[3]T[4],但T[2]==T[5]

next[6]= -1 根據 (2) 雖前面有abC=abC,但T[3]==T[6]

next[7]=4 根據 (3) 前面有abCa=abCa,且 T[4]!=T[7]

若T[4]==T[7],即T=” adCadCad”,那麼将是這樣:next[7]=0, 而不是= 4,因為T[4]==T[7].

如果你覺得有點懂了,那麼

練習:求T=”AAAAAAAAAAB” 的模式函數值,并用後面的求模式函數值函數驗證。

意義:

next 函數值究竟是什麼含義,前面說過一些,這裡總結。

設在字元串S中查找模式串T,若S[m]!=T[n],那麼,取T[n]的模式函數值next[n],

1. next[n]= -1 表示S[m]和T[0]間接比較過了,不相等,下一次比較 S[m+1] 和T[0]

2. next[n]=0 表示比較過程中産生了不相等,下一次比較 S[m] 和T[0]。

3. next[n]= k >0 但k<n, 表示,S[m]的前k個字元與T中的開始k個字元已經間接比較相等了,下一次比較S[m]和T[k]相等嗎?

4. 其他值,不可能。

四. 求串T的模式值next[n]的函數

void get_nextval(const char *T, int next[])
{
    // 求模式串T的next函數值并存入數組 next。
    int j = 0, k = -1;
    next[0] = -1;
    while (T[j] != '/0')
    {
        if (k == -1 || T[j] == T[k])
        {
            ++j;
            ++k;
            if (T[j] != T[k])
                next[j] = k;
            else
                next[j] = next[k];
        }// if
        else
            k = next[k];
    }// while

}

//下面是KMP模式比對程式


int KMP(const char *Text, const char* Pattern) //const 表示函數内部不會改變這個參數的值。
{
    if (!Text || !Pattern || Pattern[0] == '\0' || Text[0] == '\0')//
        return -1;//空指針或空串,傳回-1。
    int len = 0;
    const char *c = Pattern;
    while (*c++ != '\0')//移動指針比移動下标快。
        ++len;//字元串長度。
    int *next = new int[len + 1];
    get_nextval(Pattern, next);//求Pattern的next函數值
    int index = 0, i = 0, j = 0;
    while (Text[i] != '\0' && Pattern[j] != '\0')
    {
        if (Text[i] == Pattern[j])
        {
            ++i;// 繼續比較後繼字元
            ++j;
        }
        else
        {
            index += j - next[j];
            if (next[j] != -1)
                j = next[j];// 模式串向右移動
            else
            {
                j = 0;
                ++i;
            }
        }
    }//while
    delete[]next;
    if (Pattern[j] == '\0')
        return index;// 比對成功,傳回比對首字元下标
    else
        return -1;
}
//測試程式:


int main()
{
    char* text = "abcdefgh123456789465asdac789asd4654qw5e46a1";
    char*pattern = "4654qw";
    int pos = KMP(text, pattern);
    if (pos == -1)
        printf("無法找到比對字元串!\n");
    else
        printf("比對成功!比對首字元下标:%d\n", pos);
    system("pause");
    return 0;
}      
int kmp_find(const string& target,const string& pattern)
{
    const int target_length = target.size();
    const int pattern_length = pattern.size();
    int* overlay_value = new int[pattern_length];
    int index;
    overlay_value[0] = -1;
    for (int i = 1; i < pattern_length; i++)
    {
        index = overlay_value[i - 1];  //存儲先前比對失敗的位置
        while (index >= 0 && pattern[i] != pattern[index + 1])  // pattern[i]!=pattern[index+1]
        {
            index = overlay_value[index];  //
        }
        if (pattern[i] == pattern[index + 1])
        {
            overlay_value[i] = index + 1;
        }
        else
        {
            overlay_value[i] = -1;
        }
    }
    //比對算法
    int pattern_index = 0;
    int target_index = 0;
    while (pattern_index<pattern_length&&target_index<target_length)
    {
        if (target[target_index]==pattern[pattern_index])
        {
            ++target_index;
            ++pattern_index;
        }
        else if (pattern_index==0)
        {
            ++target_index;
        }
        else
        {
            pattern_index = overlay_value[pattern_index - 1] + 1;
        }
    }
    if (pattern_index == pattern_length)
    {
        return target_index - pattern_index;
    }
    else
        return -1;
    delete[] overlay_value;

}

int main()
{
    string source = "annbcdanacadsannannabnna";
    string pattern = "annacanna";
    cout << "第一種方法:" << Index_BF("banananobano", "nano", 0) << endl;

    cout << "第二種方法:" << matchString(source, pattern) << endl;

    cout << "kmp:" << kmp_find(source,pattern) << endl;;
    string pattern1 = "abaabcaba";
    compute_overlay(pattern1);

    return 0;
}      
參考:http://blog.csdn.net/caoyan_12727/article/details/52556327



      

C/C++基本文法學習

STL

C++ primer