KMP算法

以下说明摘自维基百科

在计算机科学中,Knuth-Morris-Pratt 字符串查找算法(常简称为“KMP算法”)可在一个主“文本字符串”S内查找一个“词”W的出现位置。此算法通过运用对这个词在不匹配时本身就包含足够的信息来确定下一个匹配将在哪里开始的发现,从而避免重新检查先前匹配的字符。

这个算法是由高德纳(Donald Ervin Knuth)和沃恩·普拉特在1974年构思,同年詹姆斯·H·莫里斯也独立地设计出该算法,最终由三人于1977年联合发表。

注意:在本文中,我们将使用始于零的数组来表示我们的字符串。所以在下面例子中,我们用W[2]来表示字符串W中的字符'C'。这种表示遵从C语言的语法。


算法说明

查找算法实例

让我们用一个实例来演示这个算法。在任意给定时间,本算法被两个整数mi所决定:

  • m代表主文字符串S内匹配字符串W的当前查找位置,
  • i代表匹配字符串W当前做比较的字符位置。

图示如下:

             1         2  
m: 01234567890123456789012
S: ABC ABCDAB ABCDABCDABDE
W: ABCDABD
i: 0123456

我们从WS的开头比较起。我们比对到S[3](=' ')时,发现W[3](='D')与其不符。接着并不是从S[1]比较下去。我们已经知道S[1]~S[3]不与W[0]相合。因此,略过这些字元,令m = 4以及i = 0。

             1         2  
m: 01234567890123456789012
S: ABC ABCDAB ABCDABCDABDE
W:     ABCDABD
i:     0123456

如上所示,我们检核了"ABCDAB"这个字串。然而,这与目标仍有些差异。我们可以注意到,"AB"在字串头尾处出现了两次。这意味着尾端的"AB"可以作为下次比较的起始点。因此,我们令m = 8, i = 2,继续比较。图示如下:

             1         2  
m: 01234567890123456789012
S: ABC ABCDAB ABCDABCDABDE
W:         ABCDABD
i:         0123456

m = 10的地方,又出现不相符的情况。类似地,令 m = 11, i = 0继续比较:

             1         2  
m: 01234567890123456789012
S: ABC ABCDAB ABCDABCDABDE
W:            ABCDABD
i:            0123456

这时,S[17](='C')不与W[6]相同,但是亦出现了两次"AB",我们采取一贯的作法,令m = 15i = 2 ,继续搜寻。

             1         2  
m: 01234567890123456789012
S: ABC ABCDAB ABCDABCDABDE
W:                ABCDABD
i:                0123456

我们找到完全匹配的字串了,其起始位置于S[15]的地方。

“部分匹配”表(又叫做“失配函数”)

表的作用是让算法无需多次匹配S中的任何字符。能够实现线性时间搜索的关键是在主串的一些字段中检查模式串的初始字段,我们可以确切地知道在当前位置之前的一个潜在匹配的位置。换句话说,在不错过任何潜在匹配的情况下,我们"预搜索"这个模式串本身并将其译成一个包含所有可能失配的位置对应可以绕过最多无效字符的列表。

对于W中的任何位置,我们都希望能够查询那个位置前(不包括那个位置)有可能的W的最长初始字段的长度,而不是从W[0]开始失配的整个字段,这长度就是我们查找下一个匹配时回退的距离。因此T[i]W的可能的适当初始字段同时也是结束于W[i - 1]的子串的最大长度。我们使空串长度是0。当一个失配出现在模式串的最开始,这是特殊情况(无法回退),我们设置T[0] = -1,在下面讨论。

建立表算法示例

我们首先考虑例子W = "ABCDABD"。使用这个大致相同的模式串作为主搜索,我们将会看到它高效的原因。

首先,我们设定T[0] = -1。为了找到T[1],我们必须找到一个"A"的适当后缀同时也是W的前缀。但"A"没有后缀,所以我们设定T[1] = 0。类似地,T[2] = 0

继续到T[3],我们注意到检查所有后缀有一个捷径:假设我们发现了一个适当后缀,结束于W[2]、长度为2(最大可能)的后缀,那么它的第一个字符是W的一个适当前缀。因此一个结束于W[1]的适当前缀,我们已经确定了不可能出现在T[2]。因此在每一层递推中,这个规则是只有在上一层找到一个长度为m的有效后缀时,才需要需要检查给定长度为m+1的后缀(例如,T[x]=m)。

那么我们甚至不需要关心具有长度为2的子串,由于上一个情况因长度为1而失配,所以T[3] = 0

我们继续遍历到W[4]子序列,'A'。同样的逻辑说明我们需要考虑的最长子串的长度是1,并且在'A'这个情况中有效,回退到我们寻找的当前字符之前的字段,因此T[4] = 0

现在考虑下一个字符W[5]'B',我们使用这样的逻辑:如果我们曾发现一个子模式在上一个字符W[4]之前出现,继续到当前字符W[5],那么在它之前它本身会拥有一个结束于W[4]合适的初始段,与事实相反的是我们已经找到'A'是最早出现在结束于W[4]的合适字段。因此为了找到W[5]的终止串,我们不需要查看W[4]。因此T[5] = 1

最后,我们看到W[4] = 'A'下一个字符是'B',并且这也确实是W[5]。此外,上面的相同参数说明为了查找W[6]的字段,我们不需要向前查看W[4],所以我们得出T[6] = 2

于是我们得到下面的表:

i 0 1 2 3 4 5 6
W[i] A B C D A B D
T[i] -1 0 0 0 0 1 2

建立表的算法的效率

建立表的算法的复杂度是O(n),其中nW的长度。除去一些初始化的工作,所有工作都是在while循环中完成的,足够说明这个循环执行用了O(n)的时间,同时还会检查pospos - cnd的大小。在第一个分支里,pos - cnd被保留,而poscnd同时递增,自然,pos增加了。在第二个分支里,cndT[cnd]所替代,即以上总是严格低于cnd,从而增加了pos - cnd。在第三个分支里,pos增加了,而cnd没有,所以pospos - cnd都增加了。因为pos ≥ pos - cnd,即在每一个阶段要么pos增加,要么pos的一个下界增加;所以既然此算法只要有pos = n就终止了,这个循环必然最多在2n次迭代后终止,因为pos - cnd1开始。因此建立表的算法的复杂度是O(n)

代码实现

main.

#include 
#include 

void getMatchingTable(char * pattern , int length, int * matchTable)
{
    if( pattern==NULL || length<=0 || matchTable==NULL)
        return;
    
    int j = 0, k = -1;
    matchTable[0] = k;
    while (j < (int)length - 1)
    {
        if (-1 == k || pattern[j] == pattern[k])
        {
            matchTable[++j] = ++k;
        }
        else
        {
            k = matchTable[k];
        }
    }
    return;
}

int kmpMatch(char * target, int targetLength, char * pattern, int patternLength)
{
    int i = 0, j = 0, index = 0;
    int matchTable[256] = {0};
    getMatchingTable(pattern, patternLength,matchTable);
    
    while (i < targetLength && j < patternLength)
    {
        if (-1 == j || target[i] == pattern[j])
        {
            i++;
            j++;
        }
        else
        {
            j = matchTable[j];
        }
    }
    
    if (j >= patternLength )
        index = i - patternLength;
    else
        index = -1;
    
    return index;
}

int main(int argc, const char * argv[])
{
    // insert code here...
    char target[] = "ABC ABCDAB ABCDABCDABDE";
    char pattern[] = "ABCDABD";
    int index = kmpMatch(target, 23, pattern, 7 );
    
    if ( index != -1 )
    {
        printf("\"%s\" is in the Pos = %d of \"%s\"\n",pattern, index,target);
    }
    else
    {
        printf("\"%s\" is not in the \"%s\"\n",pattern,target);
    }
    
    return 0;
}




另见

  • Boyer-Moore字符串搜索算法

外部链接

  • (中文)从头到尾理解KMP算法by saturnman
  • (中文)KMP算法详解by Matrix67
  • (中文)kmp算法简明教程by caochao
  • (中文)从头到尾彻底理解KMPby July
  • (英文)An explanation of the algorithm and sample C++ code by David Eppstein
  • (英文)Knuth-Morris-Pratt algorithm description and C code by Christian Charras and Thierry Lecroq
  • (英文)Interactive animation for Knuth-Morris-Pratt algorithm by Mike Goodrich
  • (英文)Explanation of the algorithm from scratch by FH Flensburg.

引用

  • 高德纳; James H. Morris, Jr, Vaughan Pratt. Fast pattern matching in strings. SIAM Journal on Computing. 1977, 6 (2): 323–350.
  • Thomas H. Cormen; Charles E. Leiserson, Ronald L. Rivest, Clifford Stein. Section 32.4: The Knuth-Morris-Pratt algorithm. Introduction to Algorithms Second edition. MIT Press and McGraw-Hill. 2001: 923–931. ISBN 978-0-262-03293-3.

你可能感兴趣的:(算法,字符串匹配,算法,字符串匹配,KMP)