KMP算法原理

背景与原理

KMP算法与ac自动机一样,以其发现者作为算法名称。同样的,作为字符串的匹配优化算法,他的着重点仍然在于减少对重复前缀或者后缀的匹配次数,从而提高搜索的效率。
和ac自动机一样,KMP算法也是用一个失败跳转的规则来快速跳转。不同的是,由于KMP针对的是单字串的匹配,他不需要ac自动机那样用字典树来进行跨字串的跳转,只需要跳转到本字串的其他字符。因此,在通常情况下,KMP的跳转甚至只需要用一个整数来记录失败跳转的位置。

实例分析

假设有模式串:abcdabd, 有待匹配字串bbcabcdababcdabcdabde,我们来分析KMP算法的跳转。
首先第一步匹配,模式串第一位a与匹配串第一位b不能匹配,模式串后移一位:

b b c a b c d a b a b c d a b c d a b d e
|
a b c d a b d

同样模式串第一位a与匹配串第二位b不匹配,模式串继续后移直到能够匹配的状态:

b b c a b c d a b a b c d a b c d a b d e
* * * | | | | | | 
. . . a b c d a b d

此时,发现模式串最后一位d与匹配串不能匹配,那么这里就会出现这样一个问题:比较暴力的做法是模式串继续后移1位进行匹配,这样当然能够达到搜索的效果,但是由于他抛弃了之前匹配串的特性,效率显得比较低下;另外一种思路,根据之前匹配的结果,我们不再是向后简单的移动1位,而是移动到一个与之前匹配完全不相关的位置,从而加速匹配。
比如,我们可以采用一个补救的方法,假设数据串模式串中都是不同的字符,那么我们对于与字符串匹配上的这个区间(abcdab)内的所有字串不需要再次匹配,可以直接跳过。然而实际上匹配串中出现了重复的字串,为了防止漏检,我们需要计算一个补救值快速定位到重复位置,我们称之为跳转。
上例中,在d处匹配失败,我们先移动到d的位置,在向前移动到之前的重复字符a处:

b b c a b c d a b a b c d a b c d a b d e
* * * * * * * | | 
. . . . . . . a b c d a b d (+6-2)

实际上,我们可以根据后缀数组的最长相同前缀来进行跳转位置的计算。在匹配之前预先计算好所有的跳转位置的计算,那么我们可以快速跳转,缩短匹配时间。

实现

/* kmp.h */
#include <vector>
#include <iostream>

//比较规则,用来比较数据单元是否相同
class comparePolicy {
    virtual int comp(void *, void *) = 0;
};

template <typename T, class Policy>
class kmp {
public:
    kmp()
    : arrayOffset { nullptr }
    , arraySize { 0 }
    {}

    ~kmp() {
        if (arrayOffset) {
            delete []arrayOffset;
            arrayOffset = nullptr;
            arraySize = 0;
        }
    }

    void Create(T *array, int len) {
        if (arrayOffset) {
            delete []arrayOffset;
            arrayOffset = nullptr;
            arraySize = 0;
            arrayData = nullptr;
        }
        if ( len < 1) 
            return ;

        arrayOffset = new int [len] {};
        arraySize = len;
        arrayData = array;

        buildFail();
    }

    std::vector<int> Search(T * pVal, int iLen) {
        Policy p {};
        std::vector<int> vRet{};
        if (iLen < arraySize) {
            return std::move(vRet);
        }
        int pos = 0;
        for (; pos < iLen; ) {
            int subpos = 0;
            for (; subpos < arraySize; ) {
                if (p.comp(pVal + pos + subpos, arrayData + subpos) == 0) {
                    subpos++;
                }
                else {
                    break;
                }
            }
            if (subpos == arraySize) {
                //如果有匹配上的,我们借用最后一步的失配(实际上是一样的)
                vRet.push_back(pos);
                subpos--;
            }
            pos += subpos - arrayOffset[subpos];

        }

        return std::move(vRet);

    }

private:
    void buildFail() {
        //后缀列表,起始pos + len
        T *suffix = arrayData + 1;
        int suffix_len = arraySize - 1;
        Policy p {};
        arrayOffset[0] = -1;
        while (suffix_len > 1) {
            int pos = 0;
            //寻找最长匹配串长度
            while (p.comp(arrayData + pos, suffix + pos) == 0) {
                pos++;
                if (pos == suffix_len) {
                    break;
                }
            }
            if (pos == 0) {//不匹配
                arrayOffset[arraySize - suffix_len + 1] = 0;
                suffix++;
                suffix_len --;
            }
            else {
                //另外一种情况下,我们给他们加上匹配的长度位置
                for (int i = 1; i <= pos; ++i) {
                    arrayOffset[arraySize - suffix_len + i ] = i;
                }
                suffix += pos;
                suffix_len -= pos;
            }

        }

    }


private:
    T *arrayData;
    int *arrayOffset;
    int arraySize;
};
/* test.cpp */
int main() {
    class CharCompare : public comparePolicy {
    public:
        virtual int comp(void *a, void *b) {
            return *(char *)a - *(char *)b;
        }
    };

    char test[] = "abcdabc";
    char test_string[] = "abcdabcdabcdabcdabd abcd";

    kmp<char, CharCompare> kmp_sample {};
    kmp_sample.Create(test, strlen(test));
    auto result = kmp_sample.Search(test_string, strlen(test_string));

    cout << test_string << endl;
    for(auto k : result) {
        for(int i = 0; i < k; i++) {
            cout << "-";
        }
        cout << test << endl;
    }

    return 0;
}

得到的结果如下:

abcdabcdabcdabcdabd abcd
abcdabc
----abcdabc
--------abcdabc
right