算法 KMP算法

解决问题:

      KMP算法主要解决的问题就是在字符串(主串)中的模式(pattern)定位问题。记主串为T,模式串为P,则KMP算法就是返回P在T中出现的具体位置,如果没有出现则返回-1。

      如图所示,对于这一问题,最简单的思路就是:从左至右一个个匹配,如果在这个过程中有某个字符不匹配,则从当前主串匹配的起始位的下一位开始重新匹配。具体过程如下图所示。
初始化:

如果 i 指针指向的字符和 j 指针指向的字符不一致,那么把 i 右移1位,j 从0位开始,从新开始匹配:

如果 i 指针指向的字符和 j 指针指向的字符一致,则 i 与 j 都向后移动

基于以上想法得到了以下程序:

/*
暴力破解法

ts 主串

ps 模式串

如果找到,返回在主串中第一个字符出现的下标,否则为-1

 */

class Solution{
public:
    int bf(string t, string p) {
        int i = 0; // 主串的位置
        int j = 0; // 模式串的位置
        while(i < t.length() && j < p.length()){
            if (t[i] == p[j]) { // 当两个字符相同,就比较下一个
                i++;
                j++;
            }else{
                i = i - j + 1; // 一旦不匹配,i后退
                j = 0; // j归0
            }
        }
        if(j == p.length()){
            return i - j;
        }else{
            return -1;
        }
    }
};

      暴力法能够解决模式串匹配的问题,但不够好。主要的问题在于匹配失败之后我们对主串进行了回退(即:i = i - j + 1)。以下图为例:

      此时 C 与 B 不匹配,如果按照暴力破解的方法,我们会把i置于第1位,但如果我们要保持 i 不回退,我们应该怎么做。KMP算法的思路是:“利用已经部分匹配这个有效信息,保持i指针不回溯,通过修改j指针,让模式串尽量地移动到有效的位置。”

在这通过观察可知合理做法是把j移动到2位。为什么?

      直观的说是因为前面存在 A B 相同。而根本的原因在与:由于不匹配的是 C 与 B,这说明在这之前P串与T串是匹配的(均为:A B C A B),而对于 A B C A B 这一部分存在着前缀(A B)与后缀(A B)相等,因此在T串与P串中存在着相同的前缀与后缀。故P串的前缀与T串的后缀相同,所以我们将j的位置调整至相等前缀的后一位。
用数学表达就是:
      当:T[i] != P[i] 时,有T{ [i - j] ~ [i - 1] } = P{[0] ~ [j - 1]}
      同时如有P{ [0] ~ [k - 1] } = P{ [j - k] ~ [j - 1] }
      必有:T{ [i-k] ~ [i-1] } = P{ [0] ~ [k - 1] }
结合下面图片理解:

      上述的存在相等的前缀与后缀解释了:调整 j 的位置后 i ,j 之前字符串仍然匹配的原因,下面介绍不修改 i 位置的正确性。以下图为例:

      首先,不修改 i 的位置相当于去除了以 i = 1 (i - j+1) ~4 (i - 1) 为起始位置的所有情况,那么证明不修改 i 位置的正确性可以等价与证明从以(i - j+1)~(i - 1)开头不存在可能解。可能解即P串与T串匹配。对模式串P的{ [0] ~ [j - 1] }分析,如果存在某一前缀与后缀相同则必然从T中后缀开始处匹配P的前缀才能一直匹配至原不匹配处。

      从上述的分析可知想让 i 不后退,需要知道 j 调整到位置,而 j 调整的位置取决于P{ [0] ~ [j - 1] } 中最长前缀与后缀相等的位置,如有P{ [0] ~ [k - 1] } = P{ [j - k] ~ [j - 1] },那么 j 就调整到 k 位。我们用数组保存这一跳转信息,称之为next数组。

next数组定义为:匹配模式串j位置失配,j应调整到的新位置。根据 j 的调整方法与模式串前 j-1 位最长前后缀相等的关系,可知next[j]的值应为模式串前 j - 1 位最长前后缀相等的前缀末尾下一位。

下面给出next数组求法和解释:

class Solution{
public:
    vector<int> next;
    vector<int> getNext(string p){
    int n = p.length();
    next.resize(n);
    next[0] = -1;
    int j = 0;//后缀待判断位
    int k = -1;//前缀待判断位
    while(j < n - 1){
        if(k == -1 || p[j] == p[k]){
            next[++j] = ++k;
        }else {
            k = next[k];
        }
    }
    return next;
	}
};

首先,重述一遍next[j]的含义:当P[j] != T[i]时,j指针的下一步移动位置。同时也是对模式串P前 j - 1 位最长前后缀相等时前缀末尾的下一位

初始化:当 j 为0时,如果这时候不匹配,怎么办?

j已经不能再左移了,也就是i在这一位已经不可能与模式串匹配了,这时候说明 i 可以直接右移一位。为了与P[j] =T[i]情况一起处理(i++,j++),定义next[0] = -1,得 i 指向下一位,j 为0。

当 j 为1时,如果这时候不匹配?

显然,j指针一定后移到0的位置。

再重述一遍next[j]的含义:当P[j] != T[i]时,j指针的下一步移动位置。同时也是对模式串P前 j - 1 位最长前后缀相等时前缀末尾的下一位

一般情况,由于我们的next数组是按顺序初始化的,假设我们现在在处理前缀位k与后缀位i,求next[j + 1],如下图所示:

因为比对到了 k 与 j ,说明P{ [0] ~ [k - 1] } 与 P{ [j - k] ~ [j - 1] }匹配(next[j] = k)
如果P[k] == P[j],那么P{ [0] ~ [k] } 与 P{ [j - k] ~ [j] }匹配,那么next[j+1] = k + 1=next[j] + 1(因为next[j+1]表示在前j位中最长前后缀相等的前缀末尾的下一位也就是k+1)。

如果P[k] != P[j],说明此时前j位的最长前缀到不了第 k 位,为了求next[j+1],我们要继续寻找P[?] = P[j],
在这里插入图片描述
图示A1与A2是相等的前后缀,由于P[k] != P[j]所以我们继续寻找前 j 位的最长前后缀。我们取出next[k]所在的位置,由next数组的定义,B1与B2相等,而由A1与A2相等,故存在B3与B2相等=>B1与B3相等,那么我们就可以继续判断P[j] 是否等于 P[next[k]],如果相等就可以得到next[j + 1],不等继续递归,直到k=-1,执行i++,k++。

好了,有了next数组之后我们就可以写KMP算法了:

class Solution{
public:
    vector<int> next;
    vector<int> getNext(string p){
    int n = p.length();
    next.resize(n);
    next[0] = -1;
    int j = 0;
    int k = -1;
    while(j < n - 1){
        if(k == -1 || p[j] == p[k]){
            next[++j] = ++k;
        }else {
            k = next[k];
        }
    }
    return next;
    }
    int KMP(string t, string p){
        int i = 0; // 主串的位置
        int j = 0; // 模式串的位置
        next.clear();
        getNext(p);
        while(i < t.length() && j < p.length()){
            if(j == -1 || t[i] == p[j]){
                i++;
                j++;
            }else{
                j = next[j]; // j调整到合适位置
            }
        }
        if(j == p.length()){
            return i - j;
        }else{
            return -1;
        }
    }
};

最后引用WNJXYK大佬的模板:
build为构建next数组,改进的地方为把所有匹配解存入容器中返回
具体讲解可通过链接查看


namespace KMP{
    vector<int> next;
 
    void build(const string &pattern){
        int n = pattern.length();
        next.resize(n + 1);
        for (int i = 0, j = next[0] = -1; i < n; next[++i] = ++j){
            while(~j && pattern[j] != pattern[i]) j = next[j];
        }
    }
 
    vector<int> match(const string &pattern, const string &text){
        vector<int> res;
        int n = pattern.length(), m = text.length();
        build(pattern);
        for (int i = 0, j = 0; i < m; ++i){
            while(j > 0 && text[i] != pattern[j]) j = next[j];
            if (text[i] == pattern[j]) ++j;
            if (j == n) res.push_back(i - n + 1), j = next[j];
        }
        return res;
    }
};

参考:
详解KMP算法
KMP算法的Next数组详解
如果next数组分析还是不清晰可以看看参考里唐小喵的next数组详解

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值