KMP算法(暴力破解法的优化以及求next数组)

10 篇文章 0 订阅
1 篇文章 0 订阅

前言

KMP算法是一种改进的字符串匹配算法,由D.E.Knuth,J.H.Morris和V.R.Pratt提出的,因此人们称它为克努特—莫里斯—普拉特操作(简称KMP算法)。KMP算法的核心是利用匹配失败后的信息,尽量减少模式串与主串的匹配次数以达到快速匹配的目的。具体实现就是通过一个next()函数实现,函数本身包含了模式串的局部匹配信息。KMP算法的时间复杂度O(m+n) 。
问题是:在主串S中找到第一次出现完整子串P时的起始位置。
说简单点就是我们平时常说的关键字搜索。模式串就是关键字,如果它在一个主串中出现,就返回它的具体位置,否则不返回或者返回-1。


题目概述:

给定一个模式串 S,以及一个模板串 P,所有字符串中只包含大小写英文字母以及阿拉伯数字。
模板串 P 在模式串 S 中多次作为子串出现。
求出模板串 P 在模式串 S 中所有出现的位置的起始下标。


暴力解法

**思路:**设置两个指针分别指向模式串和模板串,两个指针从左到右一个个匹配,如果这个过程中有某个字符不匹配,模板串指针就跳回到第一位,模式串指针向右移动一位。
图解:
从头开始匹配:
在这里插入图片描述
指针只想字符相同,两个指针同时向后移动直到指针不匹配时:
在这里插入图片描述
模板串回到起点,模式串指针指向下一位:
在这里插入图片描述
以此类推,直到模式串指针移到只剩模板串的长度。


暴力破解代码

// 暴力破解法
#include <iostream>
using namespace std;

int main(){
    int n, m;
    string s, p;
    cin >> n >> p >> m >> s;
    for(int i = 0, j = 0; i < m - n + 1; i ++){
        int temp = i;
        while(p[j] && s[temp] == p[j]) j++,temp ++;
        if(!p[j]) cout << temp - n << ' ';
        j = 0;
    }
   return 0;
}

暴力破解法的问题及其优化:

参考暴力破解法,我们以串中的位置指针i,j来说明,第一个位置下标以0开始,我们称为第0位。在每次匹配结束的时候两个指针都有回溯,其中回溯就是优化的关键所在。如果是我们人为来检查的话,我们很明显可以发现,因为主串匹配失败的位置前面和主串都匹配,所以显然大部分时候如果如果主串指针右移一位的话就会不匹配。因为我们已经知道前面三个字符都是匹配的,那我们就可以利用这个信息找出接下来指针该移动的位置。有一个想法,i可以不动,我们只需要移动j即可,下图例举了多种情况:
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
所以优化算法的思路有了,还需要解决的问题就是j指针有移动的位置在哪里?
分析上面的图可以发现,j移动的位置不为1(初始位置)的情况都是存在相同的子串(j指针前模版串P的子串,且相同的两个子串正是该子串的相同长度的前缀和后缀)。也就是最前面的k个字符和j之前的最后k个字符是一样的。 k为相同子串的长度。
所以现在就比较好理解为什么j为什么要这么移动了。j这样移动的话就是根据前一次匹配相同的字符串得到的新的信息。
我们可以这样理解,假设i,j到第k位后不匹配,那么我们可以得到的信息就是在第0位到第k-1位模式串与模板串相同。然后如果模板串P存在前后缀(取最长)相同那么这前后缀肯定相同,我们假定该前缀与后缀长度为l,所以P[0 ~ l-1]和P[j-l-1 ~ j-1]相同,且S[i-k-1 ~ i-k-1+l]和S[i-l-1 ~ i-1]相同,即图中的1,2,3相同。所以j可以直接移到模版串的第l位继续与模式串匹配,此前的前l位已经是相同的了。
请添加图片描述
换言之就是:
当S[i] != P[j]时,
有S[i-j ~ i-1] == P[0 ~ j-1]
由P[0 ~ k-1] == P[j-k ~ j-1] (3 == 2)
必然:S[i-k ~ i-1] == P[0 ~ k-1] (1 == 2)


所以,我们需要找出当出现不匹配的情况j指针需要移动的位置,这个位置显然是由模板串本身决定的。因此我们定义一个长度为模板串长度的next[]数组用来专门存放模板串在某一位置发生不匹配的时候应回溯到的位置。若子串的前缀集和后缀集中,重复的最长子串的长度为k,则下次匹配子串的j可以移动到第k位(下标为0为第0位),next[]存放的也正是k,对应下标是模板串与模式串不匹配的位置。


求next数组

next数组定义:因为在P的每一个位置都可能发生不匹配,也就是说我们要计算每一个位置j对应的k,所以用一个数组next来保存,next[j] = k,表示当S[i] != P[j]时,j指针的下一个位置。注下标从0开始的,k值实际是j位前的子串的最大重复子串的长度。


求next数组代码:

//求next[]数组,从2开始求,next[0],next[1]都为0
for(int i = 2, j = 0; i <= n; i ++){
	//此时的j为上一位字符的next数组值,j不为0,则说明i指针前面有j位相同
	//j存在且当前字符不匹配的话,j会等于第j个字符的next值
	//用while循环是因为相同的子串里面可能还有相同的子串
	//一直循环到有相同的子串或者找到j为0的时候退出循环
	//相同的子串不一定是最大字串,还有可能是当前相同子串的相同子串,像是在套娃,其实就是类似二分找出口
	//( j == 0)没有相同的子串
	// j!=0 或者i与j+1指向字符相同,存在长度为j的相同子串
	while(j && p[i] != p[j+1]) j = ne[j];
	//当前i指针与j+1指针指向字符相同,最大重复子串的长度 + 1
	//上一个循环while已经求得前面有j个字符相同
	if(p[i] == p[j+1]) j++;
	//给该位置的next数组赋值
	ne[i] = j;
}

匹配字符串

将模板串与模式串进行匹配,匹配不符是j = next[i],如果匹配成功则输出对应的位置。匹配方法与求next数组方法相同。
匹配字符串代码:

for(int i = 1, j = 0; i <= m; i ++){
	while(j && s[i] != p[j+1]) j = ne[j];
    if(s[i] == p[j+1]) j++;
   	if(j == n){//
       	cout <<(i - n)<< ' ';
       	j = ne[j];//下一位从P的最大相同子串开始比较
    }
}

完整代码:

// KMP算法 C++
#include <iostream>
using namespace std;

const int N = 100010, M = 1000010; //N为模式串长度,M匹配串长度
int n, m;  // n < m
int ne[N]; //next[]数组,避免和头文件next冲突
char s[M], p[N];  //s为模式串, p为匹配串

int main(){
    cin >> n >> p+1 >> m >> s+1;  //下标从1开始存储
    //求next[]数组,从2开始求,next[0],next[1]都为0
    for(int i = 2, j = 0; i <= n; i ++){
        while(j && p[i] != p[j+1]) j = ne[j];
        if(p[i] == p[j+1]) j++;
        ne[i] = j;
    }
    //匹配字符串
    for(int i = 1, j = 0; i <= m; i ++){
        while(j && s[i] != p[j+1]) j = ne[j];
        if(s[i] == p[j+1]) j++;
        if(j == n) {
            cout <<(i - n)<< ' ';
            j = ne[j];
        }
    }
    return 0;
}

结语

弄了好才整明白这些东西,但是感觉表达的不是很清楚,可能存在一些错误,请大佬们多多指教。

  • 3
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

江山酒

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值