kmp算法

目录

kmp算法的介绍

kmp算法的前置思路

kmp算法的眼睛

kmp算法的next数组

kmp算法中的归纳和构造两种思想

kmp算法完整代码实现

next数组的小优化


kmp算法的介绍

        KMP算法是一种改进的字符串匹配算法,由D.E.Knuth,J.H.Morris和V.R.Pratt提出的,因此人们称它为克努特—莫里斯—普拉特操作(简称KMP算法)。KMP算法的核心是利用匹配失败后的信息,尽量减少模式串与主串的匹配次数以达到快速匹配的目的。具体实现就是通过一个next()函数实现,函数本身包含了模式串的局部匹配信息。KMP算法的时间复杂度O(m+n)

                                                                                                                                ——来自百度 

kmp算法的前置思路

        要从一个字符串(主串)中检查是否含有另一个字符串(模式串)并返回他的位置,首先想到的是暴力匹配,也就是让模式串从第一个字符开始,依次和主串的每一个字符比对,不一样就往右挪,一样就接着比对模式串第二个字符。当我们真的拿着两个物件(就比如一长一短两把尺子吧)这样比对了,会发现一步一步往右挪是不太聪明的。因为明明通常可以跳好几步,为什么每次只挪一步呢?那应该挪几步呢?    

主串值abababcdcd
下标0123456789
子串值ababcd
下标012345

        上图在模式串下标4处不匹配了,于是模拟一下这个动作,就想象我们手里拿着模式串,往主串上靠,现在比对出下标4处的内容不一致了,我们手里拿着的模式串应该重新往右放到什么位置呢?首先,我们能感觉到如果直接拿着模式串的最左端对齐主串下标4处,是有可能错过一些情况的,而由于主串中红色部分一定不如模式串长,所以模式串最终还是要在4这个位置往右延申的,所以我们把模式串下标0对齐主串4,再从4这里往左挪一挪模式串,边挪边让模式串尽可能长的前缀主串4左面匹配。

        上述这一个手法就是kmp算法,有没有发现,我们生来就是算法人,只不过没有去总结,没有抽象出新概念,只是付出了行动而已。但只会行动不总结规律,我们就不能举一反三,总结出规律,抽象出新概念,这个概念打开我们的新视野,就像一双眼睛。

kmp算法的眼睛

        接着上一步,从4这里往左挪一挪模式串。上面绿色字体是什么?是模式串上一步匹配成功的那一部分子串的前缀,我暂且把模式串的这个子串"abab"用 temp 来表示吧,绿色字体是 temp 的前缀;上面黄色字体是啥子?是上一次匹配成功的主串部分的后缀。由于 temp 和主串这一部分是完全匹配成功的,完全一样,所以黄色字体是temp的后缀呀!所以这是从4这里往左挪了多长?是"temp的最长公共前后缀"这么长。所以说模式串跟主串失配之后怎么调整位置?简单,因为下标是从0开始的,所以把模式串下标为“temp的最长公共前后缀长度”处的字符和当前失配点比对就ok。

        “temp的最长公共前后缀长度”我愿称之为kmp算法的眼睛:"eye"。

kmp算法的next数组

        我们用一个next数组存储模式串每一个下标处(不包括它)左侧 temp 的 eye 。为了方便描述 next 数组,我称这个 eye 为当前下标处的 eye 。比如本例子中"abab"的最长公共前后缀长度为模式串中 4 处的 eye。这样每当模式串在 j 处失配了,就如上述红字所说, j = next[j]; 

int Kmp(string& s, string& t, vector<int>& next) {
    int i = 0, j = 0;
    GetNext(t, next);
    while (i < s.size() && j < (int)t.size()) {
        if (j == -1 || s[i] == t[j]) {
            ++i; ++j;
        }
        else j = next[j];
    }
    if (j == t.size()) return i - j;
    return -1;
}

        上面 j == -1 是怎么回事 ? 能有这个疑问说明您没考虑周密,考虑周密的都卡在上一步红字那句话上了 (´ー∀ー`)  。1 处的eye是多少?他左面只有一个字符,一个字符最长公共前后缀长度是0还是1?这个我们不能定义,得让失配之后的重配操作去定义。模式串下标 j 在 1 处失配,把 j 重新赋为几? 赋为 0 啊,所以单个字符的最长公共前后缀的长度是 0 ,不光单个字符不太好理解,重叠的字符都是这个道理,简言之在kmp算法中最长公共前后缀不能取到 temp 的全部,最长就是 temp 的长度减 1。那么 0 处的 eye 是多少?一样看重配操作,当 j 在0处就直接失配,按照我们当前搭建起来的程序轮廓,j 会跑到它的 eye 处继续和主串当前位置比对,这个时候其实 j 这个位置已经没有意义了,所以 if 语句必须走进去,不能再走else了,在 if 语句中,我们需要通过旧操作实现新功能,显然当 j = 0 处失配,我们想让 j = 0 的模式串字符直接和主串下一个字符去比对,根据里面 ++i 和 ++j 的代码设计,我们把 next[0] 赋为 -1。当然,可以别这么卷,可以多写几个if,把 0 处的情况揪出来单独讨论,但是 kmp 算法就是牛的没办法。

        实现GetNext函数,当然是在函数体内为next数组填充元素。那么模式串下标 0 处我们填充 -1,在下标 1 处填充 0, 我们用两个指针用head指向下标0处,用tail指向下标1处,把tail依次自增到模式串的倒数第二个字符处,每次比对head处的字符和tail处的字符是否相等,用head的自增和回溯操作来得到tail + 1 处的 eye 。这样就得到了模式串所有下标处的 eye 把它们存到了next数组里。好的,head怎么回溯 ?一个问题不说第二遍夯,head = next[head]。但是kmp算法是大牛算法,咱假装不知道下标 1 处填充0, 咱把head初始化为 -1, tail 初始化为 0。不说了,就是秀。

void GetNext(string& t, vector<int>& next) {
    int head = -1, tail = 0;
    next[0] = -1;
    while (tail < next.size() - 1) {
        if (head == -1 || t[head] == t[tail]) {
              next[++tail] = ++head;
        }
        else {
            head = next[head];
        }
    }
}

kmp算法中的归纳和构造两种思想

        上一步里紫色文字,逗号左面是归纳,右面是构造。这个思想很牛啊,观察归纳代码设计中变量值的变化,构造出这些变量的预设值,这一步是叫预赋值还是啥的来着?这个是++j,要是j += 2就为next[0]预赋值为-2。以此类推。有了这个思想之后,应该能看出来head 初始化为 -1是怎么秀的了哈。next[++tail]有赋值为0的需求的时候head 应取 -1。

kmp算法完整代码实现

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

void GetNext(string& t, vector<int>& next) {
    int head = -1, tail = 0;
    next[0] = -1;
    while (tail < next.size() - 1) {
        if (head == -1 || t[head] == t[tail]) {
              next[++tail] = ++head;
        }
        else {
            head = next[head];
        }
    }
}

int Kmp(string& s, string& t, vector<int>& next) {
    int i = 0, j = 0;
    GetNext(t, next);
    while (i < s.size() && j < (int)t.size()) {
        if (j == -1 || s[i] == t[j]) {
            ++i; ++j;
        }
        else j = next[j];
    }
    if (j == t.size()) return i - j;
    return -1;
}

int main() {
    string s = "aaabcdefgaabcdac";
    string t = "abcda";
    vector<int> next(t.size());
    cout << Kmp(s, t, next) << endl;
    return 0;
}

next数组的小优化

        就是考虑到当模式串在 j 处和主串失配了,j 回溯到 next[j]处如果还是同一个字符,那么必然继续失配,然后再回溯到next[next[j]]处,可是万一还是同一个字符呢?你想到了什么解决方法?没错,并查集的路径压缩,我们通过及时的压缩,把两层以上的集合都扼杀在摇篮里,就是下面的内层 if 语句。

void GetDeepNext(string& t, vector<int>& next) {
    int head = -1, tail = 0;
    next[0] = -1;
    while (tail < next.size() - 1) {
        if (head == -1 || t[head] == t[tail]) {
            ++head; ++tail;
            if (t[tail] == t[head]) next[tail] = next[head];
            else next[tail] = head;
        }
        else head = next[head];
    }
}

        最后,有没有大佬教一下怎么才能既能先写标题有能让csdn的目录点击跳转正确?我先写的标题,然后往里面插入文字就把标题的位置影响了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值