KMP算法的理解及其C语言的实现

KMP的概念网上有很多介绍,核心是理解PMT(Partial Match Table,部分匹配表),而next数组是为了编程方便将PMT右移一格后得到的。关于PMT的详细概念及最长前、后缀匹配,请参考https://www.zhihu.com/question/21923021/answer/281346746,本文主要参考该文章并分享些自己的见解。

首先要注意理清概念,主串m和匹配串n。KMP的功能是要快速在主串m中找到相应的匹配串n,而next数组的生成(或者是PMT的生成),与主串m无关,所有的操作都是针对匹配串n。很多初学者对于这点比较困惑:我的目的是在主串m中找到匹配串n,怎么关键的next数组的生成会与主串无关呢?其实仔细想想就能明白:既然是“匹配”,我们要抓的是匹配串,匹配串之外的字符毫不相干,匹配串的特性才是我们关心的。

关于字符串的匹配,一般是用两个指针移动来进行的。假设主串m中用指针textIndex,匹配串中用指针patternIndex,在没有任何字符匹配成功之前,patternIndex始终指向匹配串n的第一个字符,而textIndex指针不断右移来寻找匹配的字符。当开始遇到部分匹配的字符串时,patternIndex指针才开始右移。在出现匹配失败之前,两指针的移动方式与一般的匹配算法的移动方式是一致的。而一旦出现匹配失败,如下图所示:

mismatch.png

一般的匹配算法是将patternIndex回退到0,重新指向第一个字符;textIndex也要回到最开始匹配的字符的下一个位置:



然后再让两指针所指向的字符进行对比,因此传统的匹配算法的时间复杂度为O(len(m) * len(n))。

而KMP算法不会让textIndex回退,patternIndex也不是直接回退为0:


这两点是由next数组来保证的。上述例子的PMT和next数组如下所示:


在一般的匹配算法中,如果textIndex也不回退,那上面这个例子将永远找不到正确的结果,这是因为一般匹配算法无法确定从匹配开始到匹配失败这段字符串中有没有“部分匹配串”。所谓“部分匹配串”,就是取该字符串前缀与后缀的最长公共部分,以上面这个例子为例,匹配起始的位置是index 0 (a),匹配失败的位置是index 6 (a != c),其中的“部分匹配串”是从index 2 到 index 5 (即abab),textIndex需要回退重新确定新的匹配起始位置,首先回退到index 1,明显两个指针的值不匹配,然后textIndex指向index 2 (a),匹配成功,确定新的起始位置为index 2,两指针继续移动并匹配……如果index 2 到 index 5不存在“部分匹配串”(比如为cdef),那么textIndex其实也就没必要回退(因为新的匹配起始位置绝对不会出现在当前位置的前面那部分)。

从上述过程看,正是因为一般的匹配算法无法保证匹配起始位置到匹配失败位置之间是否还会出现新的匹配起始位置,才使得textIndex必须回退。如果不想使textIndex回退,该如何解决这个问题呢?用一个数组来记录部分匹配串长度的信息,就可以解决这个问题了,这就是KMP算法。现在我们可以从另一个角度理解next数组的值了,next[i] = 0表示 i 位置前一定不存在新的匹配起始位置,textIndex无需回退;next [i] = 4表示 i 位置前存在新的匹配起始位置,并且到 i 的距离为4,因为已经匹配4个位置了,只是 i 这个位置不知能否匹配,因此textIndex依然无需回退,并且由于next记录的信息,patternIndex也无需回退到0,只需回退到index 为4的位置,大大提升了匹配效率。

在整个KMP匹配的过程中,其实可以看作主串m的后缀与匹配串n的前缀求解公共部分的过程(实质上是匹配串自己的后缀与前缀匹配)。在匹配失败阶段,主串m的新的起始位置未定,而结束位置(匹配失败的位置)已定;匹配串的起始位置已定,而结束位置未定,因此整个过程相当于求解部分匹配串的过程。

关于求解next数组的算法,本质上是求字符串的后缀与前缀的最大公共部分,只需匹配串n错开一位和自己进行比较即可求得。详细代码如下:

Code

int* getNextArray(const char* pattern)
{
    int len = strlen(pattern);

    // Next array records the new beginning index of pattern string if the character is a mismatch between main string and pattern string.
    int* next = (int*)calloc(len, sizeof(int));
    next[0] = -1;

    int i = 0;
    int newStart = -1;       // This variable not only means the new beginning index of pattern string, but also is to traverse the other pattern string.

    // Note that at first, the pattern string will stagger one position and compare itself,
    // then it will get the result which is the intersection of longest prefix substring and longest suffix substring.
    while (i < len - 1)
    {
        if (newStart == -1 || pattern[i] == pattern[newStart])
        {
            next[++i] = ++newStart;
        }
        else
        {
            // If mismatch, then move to proper position according to the next array.
            newStart = next[newStart];
        }
    }
    return next;
}

int kmp(char* text, char* pattern)
{
    int textLen = strlen(text);
    int patternLen = strlen(pattern);
    int textIndex = 0, patternIndex = 0;
    int* next = getNextArray(pattern);

    while (textIndex < textLen && patternIndex < patternLen)
    {
        // Note that "patternIndex = -1" means there is no any matching substring, so both index are all right shift and compare.
        if (patternIndex == -1 || text[textIndex] == pattern[patternIndex])
        {
            ++textIndex;
            ++patternIndex;
        }
        else
            patternIndex = next[patternIndex];
    }

    if (patternIndex == patternLen)
        return textIndex - patternIndex;
    else
        return -1;
}

时间复杂度:O(m + n),其中m为主串的长度,n为匹配串的长度。

你可能感兴趣的:(KMP算法的理解及其C语言的实现)