KMP算法

1.字符串匹配:给定两个字符串S1, S2,我们需要确定S1里面是否包含S2,换句话说就是判断S2是否是S1的子串。
我们很容易想到的方法就是暴力搜索:


从第一个字符开始匹配,如果不相等则顺序后移一位,再进行比较,伪代码为:

n = s1.size
m = s2.size
for i = 0 to n - m
    if s2[0 ... m-1] == s1[s ... s+m-1]
        return s

这种方法的时间复杂度为O(n*m),很显然,这种方法不适用来解决大规模的匹配问题。

KMP算法:(时间复杂度为O(n + m))
现在来介绍KMP算法,这种算法能够省略那些重复的匹配,使用一个数组来保存相关的信息,并且利用这个数组就可以实现跳跃重复的匹配。
KMP算法的重点就是对这个数组进行初始化,在说明如何初始化这个数组之前,我们需要知道什么叫前缀,什么叫后缀
举个例子:在字符串ABABCAB中
所有的前缀为:

A AB ABA ABAB ABABC ABABCA

所有的后缀为:

B AB CAB BCAB ABCAB BABCAB

所以前缀就是除了最后一个字符的连续子串,后缀就是除了第一个字符的连续子串。

接着我们来说这个关键数组,我们把这个数组命名为next,那么对于字符串ABABCAB来说,这个数组为:

index 0 1 2 3 4 5 6
value 0 0 1 2 0 1 2

为什么能得到这个数组:
其中index代表数组的索引,也代表这个字符串的前 index + 1 个单词组成的子串:比如 index = 3,那么子串为ABAB,index = 4,那么子串为ABABC.

我们从index = 0 开始分析 :
index = 0,子串为 A

没有前缀,没有后缀

那么 value = 0;

index = 1,子串为 AB

前缀 : A  后缀 :B

没有相同的前缀和后缀,那么value = 0;

index = 2,子串为 ABA

前缀 : A AB 后缀 :A BA

前后缀相等的子串最大长度为1,那么value = 1;

index = 3,子串为 ABAB

前缀 : A AB ABA 后缀 :B AB BAB

前后缀相等的子串最大长度为2,那么value = 2;

index = 4,子串为 ABABC

前缀 : A AB ABA ABAB 后缀 :C BC ABC BABC

没有相同的前缀和后缀,那么value = 0;

index = 5,子串为 ABABCA

前缀 : A AB ABA ABAB ABABC 后缀 :A CA BCA ABCA BABCA

前后缀相同的最大子串长度为1,那么value = 1;

index = 6,子串为 ABABCAB

前缀 : A AB ABA ABAB ABABC ABABCA 后缀 :B AB CAB BCAB ABCAB BABCAB

前后缀相同的最大子串长度为2,那么value = 2;

这样就可以把数组初始化好了。

其中value代表着:如果我们匹配字符串时发生了不匹配,那么我们可以把字符串向前跳跃的步数。跳跃相当于把前缀代替成为后缀:

target : BCABABABCAB   -->   BCABABABCAB
pattern:   ABABCAB     -->       ABABCAB

上面这个例子中:在C这里发生了不匹配,那么我们向后跳跃next[4] == 2步,为什么index是4?因为前面已经有4个字符完全匹配了。

现在知道了大致的匹配流程了,接下来就是需要书写代码来初始化这个数组,伪代码为:

size = pattern.size
next[size]
next[0] = 0
p = 0
for i = 1 to size
    while p > 0 && pattern[i] != pattern[p]
        p = next[p-1]
    if pattern[i] == pattern[p]
        p++
    next[i] = p

上面最难理解的就是 p = next[p-1]了,这一句代表着当前面已经有着匹配时,下一个字符不匹配,那么模式自身就要退到上一次的状态,重新匹配上一次的状态的下一个字符。

初始化完毕数组之后就是kmp算法本身了, 伪代码为:

t = target.size
p = pattern.size
k = 0
for i = 0 to t
    while k > 0 && target[i] != pattern[k]
        k = next[k-1]
    if pattern[k] == target[i]
        k++
    if k == p
        return i-p+1;
    return -1

两段代码很相似,因为两段代码都是对字符串进行匹配,只不过对数组初始化的时候是对模式本身进行匹配(寻找最大前后缀相同子串)

最后我们可以得到以下的c++代码:

kmp.hpp

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

class Kmp {
public:
    Kmp(){}
    ~Kmp(){}
    int getSolution(const string& total, const string& pattern);
private:
    vector<size_t> next;
    void initNext(const string& pattern);
};

int Kmp::getSolution(const string& total, const string& pattern){
    initNext(pattern);
    size_t t = total.size();
    size_t size = pattern.size();
    size_t p = 0;
    for (size_t i = 0; i < t; ++i) {
        while (p > 0 && total[i] != pattern[p]) {  //当下一个字符不匹配时
            p = next[p - 1];
        }
        if (total[i] == pattern[p]) {
            p++;
        }
        if (p == size) {
            return i - p + 1;
        }
    }
    return -1;
}

void Kmp::initNext(const string& pattern){
    size_t p = pattern.size();
    next.resize(p, 0);

    size_t k = 0;
    for (size_t i = 1; i < p; ++i) {
        while (k > 0 && pattern[i] != pattern[k]) { //当下一个字符不匹配时
            k = next[k - 1];    
        }

        if (pattern[i] == pattern[k]) {
            k++;
        }
        next[i] = k;
    }
}

main.cpp

#include "./kmp.hpp"

int main(int argc, char** argv) {
    if (argc < 3) {
        cout << "Usage: ./a.out total pattern" << endl;
        return 0;
    }

    Kmp solution;
    string total(argv[1]);
    string pattern(argv[2]);
    int result = solution.getSolution(total, pattern);

    cout << result << endl;

    return 0;
}

Mac 下面编译 : g++ -std=c++11 kmp.hpp main.cpp 通过。函数会输出第一次出现完全匹配时的target数组索引,如果没有匹配,则返回-1。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值