KMP算法學習&總結


0、廢話

 一直ym傳說中的kmp算法能以最壞線性的時間復雜度搞定字符串匹配,

開始動手看才知道kmp中的K居然是Donald.E.Knuth,《計算機程序設計藝術》的作者。

好吧,繼續ym……

1、傳統的字符串匹配算法

/* 
 * 從s中第sIndex位置開始匹配p
 * 若匹配成功,返回s中模式串p的起始index
 * 若匹配失敗,返回-1
 */
int index(const std::string &s, const std::string &p, const int sIndex = 0)
{
    int i = sIndex, j = 0;

    if (s.length() < 1 || p.length() < 1 || sIndex < 0)
    {
        return -1;
    }

    while (i != s.length() && j != p.length())
    {
        if (s[i] == p[j])
        {
            ++i;
            ++j;
        }
        else
        {
            i = i - j + 1;
            j = 0;
        }
    }
    return j == p.length() ? i - j: -1;
}

2、傳統字符串匹配算法的性能問題

用模式串P去匹配字符串S,在i=6,j=4時發生失配:

                              i=6

S: a   b   a   b   c   a   d   c   a   c   b   a   b

P:           a   b   c   a   c

                              j=4

此時,按照傳統算法,應當將P的第 1 個字符 a(j=0) 滑動到與S中第4個字符 b(i=3) 對齊再進行匹配: 

                 i=3 

S: a   b   a   b   c   a   a   d   a   c   b   a   b 

P:               a   b   c   a   c 

                 j=0 

這個過程中,對字符串S的訪問發生了“回朔”(從 i=6 移回到 i=3)。

我們不希望發生這樣的回朔,而是試圖通過盡可能的“向右滑動”模式串P,讓P中index為 j 的字符對齊到S中 i=5 的字符,然后試圖匹配S中 i=6 的字符與P中index為 j+1 的字符。

在這個測試用例中,我們直接將P向右滑動3個字符,使S中 i=5 的字符與P中 j=0 的字符對齊,再匹配S中 i=6 的字符與P中 j=1 的字符。

                               i=6

S: a   b   a   b   c   a   d   c   a   c   b   a   b

P:                        a   b   c   a   c

                          j=0

3、kmp算法的一般性討論

下面討論在一般性的情況下,如何實現在“不回朔”訪問S、僅依靠“滑動”P的前提下實現字符串匹配,即“kmp算法”

                               i=6

S: a   b   a   b   c   a   d   c   a   c   b   a   b

P:                        a   b   c   a   c

                              k=1

                                i=6

S: a   b   a   b   c   a   d   c   a   c   b   a   b

P:           a   b   c   a   c

                               j=4

對於任意的S和P,當S中index為 i 的字符和P中index為 j 的字符失配時,我們假定應當滑動P使其index為 k 的字符與S中index為 i 的字符“對齊”並繼續比較。

那么,這個 k 是多少?

我們知道,所謂的對齊,就是要讓S和P滿足以下條件(上圖中的藍色字符):

……(1)

另一方面,在失配時我們已經有了一些部分匹配結果(上圖中的綠色字符):

……(2)

由(1)、(2)可以得到:

……(3)

即如下圖所示效果:

定義next[j]=k,k表示當模式串P中index為 j 的字符與主串S中index為 i 的字符發生失配時,應將P中index為 k 的字符繼續與主串S中index為 i 的字符比較。

……(4)

按上述定義給出next數組的一個例子:

   j         0  1  2  3  4  5  6  7

   P        a   b  a  a  b  c  a   c

next[j]  -1  0  0  1  1  2  0  1

在已知next數組的前提下,字符串匹配的步驟如下:

i 和 j 分別表示在主串S和模式串P中當前正待比較的字符的index,i 的初始值為sIndex,j 的初始值為0。

在匹配過程中的每一次循環,若,i 和 j 分別增 1,

else,j 退回到 next[j]的位置,此時下一次循環是相比較。

4、kmp算法的實現

 在已知next函數的前提下,根據上面的步驟,kmp算法的實現如下: 

int kmp(const std::string& s, const std::string& p, const int sIndex = 0)
{
    std::vector<int>next(p.size());
    getNext(p, next);//獲取next數組,保存到vector中

    int i = sIndex, j = 0;
    while(i != s.length() && j != p.length())
    {
        if (j == -1 || s[i] == p[j])
        {
            ++i;
            ++j;
        }
        else
        {
            j = next[j];
        }
    }

    return j == p.length() ? i - j: -1;
}

ok,下面的問題是怎么求模式串 P 的next數組。

next數組的初始條件是next[0] = -1,設next[j] = k,則有:

那么,next[j+1]有兩種情況:

,則有:

   此時next[j+1] = next[j] + 1 = k + 1

, 如圖所示:

此時需要將P向右滑動之后繼續比較P中index為 j 的字符與index為 next[k] 的字符:

 

值得注意的是,上面的“向右滑動”本身就是一個kmp在失配情況下的滑動過程,將這個過程看 P 的自我匹配,則有:

如果,則next[j+1] = next[k] + 1;

否則,繼續將 P 向右滑動,直至匹配成功,或者不存在這樣的匹配,此時next[j+1] = 0。

 getNext函數的實現如下:

void getNext(const std::string &p, std::vector<int> &next)
{
    next.resize(p.size());
    next[0] = -1;

    int i = 0, j = -1;
    
    while (i != p.size() - 1)
    {
        //這里注意,i==0的時候實際上求的是next[1]的值,以此類推
        if (j == -1 || p[i] == p[j])
        {
            ++i;
            ++j;
            next[i] = j;
        }
        else
        {
            j = next[j];
        }
    }
}

 至此,一個完整的kmp已經實現。

5、getNext函數的進一步優化

注意到,上面的getNext函數還存在可以優化的地方,比如:

                 i=3

S: a   a   a   b   a   a   a   a   b

P: a   a   a   a   b

                 j=3

此時,i=3、j=3時發生失配,next[3]=2,此時還需要進行 3 次比較:

i=3, j=2;  

i=3, j=1;  

i=3, j=0。

而實際上,因為i=3, j=3時就已經知道a!=b,而之后的三次依舊是拿 a 和 b 比較,因此這三次比較都是多余的。

此時應當直接將P向右滑動4個字符,進行 i=4, j=0的比較。

一般而言,在getNext函數中,next[i]=j,也就是說當p[i]與S中某個字符匹配失敗的時候,用p[j]繼續與S中的這個字符比較。

如果p[i]==p[j],那么這次比較是多余的(如同上面的例子),此時應該直接使next[i]=next[j]。

完整的實現代碼如下:

void getNextUpdate(const std::string& p, std::vector<int>& next)
{
    next.resize(p.size());
    next[0] = -1;

    int i = 0, j = -1;

    while (i != p.size() - 1)
    {
        //這里注意,i==0的時候實際上求的是nextVector[1]的值,以此類推
        if (j == -1 || p[i] == p[j])
        {
            ++i;
            ++j;
            //update
            //next[i] = j;
            //注意這里是++i和++j之后的p[i]、p[j]
            next[i] = p[i] != p[j] ? j : next[j];
        }
        else
        {
            j = next[j];
        }
    }
}

對應的,只需要在kmp算法中將 getNext(p, next); 替換成 getNextUpdate(p, next); 即可。

6、時間復雜度分析

下面以getNext函數為例,分析kmp算法的時間復雜度。

 1 void getNext(const std::string& p, std::vector<int>& next)
 2 {
 3     next.resize(p.size());
 4     next[0] = -1;
 5 
 6     int i = 0, j = -1;
 7 
 8     while (i != p.size() - 1)
 9     {
10         if (j == -1 || p[i] == p[j])
11         {
12             ++i;
13             ++j;
14             next[i] = j;
15         }
16         else
17         {
18             j = next[j];
19         }
20     }
21 }

假定p.size()為m,分析其時間復雜度的困惑在於,在while里面不是每次循環都執行 ++i 操作,所以整個while的執行次數不一定為m。

換個角度,注意到在每次循環中,無論 if 還是 else 都會修改 j 的值且每次循環僅對 j 進行一次修改,所以在整個while中 j 被修改的次數即為getNext函數的時間復雜度。

每次成功匹配時,++i; ++j; , 由於 ++i 最多執行 m-1 次,故++j也最多執行 m-1 次,即 j 最多增加m-1次;

對應的,只有在 j=next[j]; 處 j 的值一定會變小,由於 j 最多增加m-1次,故 j 最多減小m-1次。

綜上所述,getNext函數的時間復雜度為O(m),若帶匹配串S的長度為n,則kmp函數的時間負責度為O(m+n)。

7、kmp的應用優勢

①快,O(m+n)的線性最壞時間復雜度;

②無需回朔訪問待匹配字符串S,所以對處理從外設輸入的龐大文件很有效,可以邊讀入邊匹配。


免責聲明!

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



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