KMP算法浅解

算法背景

KMP算法解决的问题是字符匹配,是由Knuth–Morris–Pratt共同开发出来的,这个算法把字符匹配的时间复杂度缩小到O(m+n),而空间复杂度也只有O(m),n是target的长度,m是pattern的长度,在此算法在发明之前并不是没有如此高效的算法,但是原算法比较复杂。Kmp算法优雅高效,但是实现却不难理解且代码长度很短,是优秀算法设计的典范,值得拿出来仔细分析;

算法核心

在最简单的枚举移动算法中,我们可以看到;逐步匹配的复杂度是O(mn),正好是两层嵌套;但是KMP算法的复杂度只有O(m+n) 这个是怎么做到的呢;主要是利用到了一个跳转表,也就是一个自我覆盖的表达形式;例如 字符串stringnapaspace ,space 进行匹配,当在前面的位置没匹配到的时候,它不会弱智得还会移动一个单位 patterntarget对照匹配,跳转表,就是挑战,因为它本身已经实现了自己的跳转,当自己匹配不到的时候,它就知道匹配接下来的几个小位移都是没有作用的;所以直接移动 next[j]个位置。

重点解疑

常见的问题是为什么跳转表能实现这个功能,为什么这么准确,它到底怎么实现的,还可以怎么理解。
关于这个问题,可以自己模拟一下去体会,可以发现这个规律。

注意事项

数学的来看一下定义,比如对于序列
待自我匹配序列
而没有更大的k满足这个条件,就是说要找到尽可能大k,使pattern前k字符与后k字符相匹配,k要尽可能的大,原因是如果有比较大的k存在,而我们选择较小的满足条件的k,那么当失配时,我们就会使pattern向右移动的位置变大,而较少的移动位置是存在匹配的,这样我们就会把可能匹配的结果丢失.
这里写图片描述
计算这个跳转表的方法可以采用递推,可以想象如果对于pattern的前j个字符,如果覆盖函数值为k
则对于pattern的前j+1序列字符,则有如下可能:

⑴ pattern[k+1]==pattern[j+1] 此时overlay(j+1)=k+1=overlay(j)+1

⑵ pattern[k+1]≠pattern[j+1] 此时只能在pattern前k+1个子符组所的子串中找到相应的next函数,h=next(k),如果此时pattern[h+1]==pattern[j+1],则next(j+1)=h+1否则重复(2)过程.

inline void BuildNext(const char* pattern, size_t length, unsigned int* next)  {
    unsigned int i, t;
    i = 1;
    t = 0;
    next[1] = 0;
    while(i < length + 1)  {
        while(t > 0 && pattern[i - 1] != pattern[t - 1]) {
            t = next[t];
        }
        ++t;
        ++i;
        if(pattern[i - 1] == pattern[t - 1]){
                   next[i] = next[t];
        }else{
            next[i] = t;
        }
    }
    //pattern末尾的结束符控制,用于寻找目标字符串中的所有匹配结果用
    while(t > 0 && pattern[i - 1] != pattern[t - 1]){
        t = next[t];
    }
    ++t;
    ++i;
    next[i] = t;
}

跳转表的工作

假如:A=”abababaababacb”,B=”ababacb”,我们来看看KMP是怎么工作的。我们用两个指针i和j分别表示,A[i-j+ 1..i]与B[1..j]完全相等。也就是说,i是不断增加的,随着i的增加j相应地变化,且j满足以A[i]结尾的长度为j的字符串正好匹配B串的前 j个字符(j当然越大越好),现在需要检验A[i+1]和B[j+1]的关系。当A[i+1]=B[j+1]时,i和j各加一;什么时候j=m了,我们就说B是A的子串(B串已经整完了),并且可以根据这时的i值算出匹配的位置。当A[i+1]<>B[j+1],KMP的策略是调整j的位置(减小j值)使得A[i-j+1..i]与B[1..j]保持匹配且新的B[j+1]恰好与A[i+1]匹配(从而使得i和j能继续增加)。

直接实现

KMP理解了跳转表,基本上这个过程就很前清晰了;后期关于图示法描述出来。

程序代码

#include<iostream>
#include<string>
#include<vector>
using namespace std;

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];
    overlay_value[0] = -1;
    int index = 0;
    for(int i=1;i<pattern_length;++i){
        index = overlay_value[i-1];
        while(index>=0 && pattern[index+1]!=pattern[i])
            index  = overlay_value[index];
        if(pattern[index+1]==pattern[i])
            overlay_value[i] = index +1;
        else
            overlay_value[i] = -1;
    }
    //KMP match algorithm really start ; up is for next[]
    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;
}

int main(){
    string source = "abcdefghijklmn";
    string pattern = "cdef";
    cout<<kmp_find(source,pattern)<<endl;
    return 0;
}

参考文章

KMP算法详解|martix67

从头到位理解KMP算法

KMP算法详解

原文地址:https://www.cnblogs.com/actanble/p/6713439.html