KMP算法

KMP算法:D.E.Knuth,J.H.Morris和V.R.Pratt三人发明,用于字符串匹配,即在一个字符串里找另外一个字符串第一次出现的位置。俗称看毛片算法,因为看一段时间就懂,过几天就忘。

KMP虐我千百遍,我却待她如初恋。


概念

模式串p:需要查找的字符串。

原串s:模式串在哪找呢?在原串里。

next数组,是一个只根据模式串p求出来的数组,作用是匹配过程中若p[j]与原串s的s[i]不相同则 j 应该调整为next[j],即用next[j]与s[i]比较。

next[i] : p中p[i-1]为后缀的字符串 与 p串自身的前缀的 最长公共串长度(可以有重叠部分),详情下面介绍

怎么求先不管,先看在有这个数组的情况下如何匹配。


符号表示

下文中并列的两行字符串,上面的是原串s,下面是模式串p,目的是在原串s里查找p,用'|'区分已经匹配(或者不可能匹配的)的和未匹配的,在'|'左边的为匹配的,右边第一个为正在比较的字符。

例子

假设我们要在abababaabbbb中查找abababbab(当然这是找不到的)。

对应的"abababbab"的next数组:

下标012345678
字符abababbab
next[i]-100123401

这里的next[0]=-1是我们规定的,那为什么不定为无穷大,-2之类的?为了代码少写if。-1的存在是为了让长串s的下一个字符和被查找串p的第一个字符比较,就是类似暴力匹配失败的情况。

i=1 ab|ababaabbbb
j=1 ab|ababbab

// 因为s[i]==p[j],不断增大i,j直到i=6,j=6
i=6 ababab|aabbbb
j=6 ababab|bab

// 这时s[6]和p[6]不匹配,按照暴力做法此时会将i自增,j置为0。
// 但KMP则不改变i,把j调整到一个特定位置next[j](这里是next[6]=4),然后继续匹配。
i=6 ababab|aabbbb
j=4   abab|abbab

// 当匹配到i=7,j=5时,再次不匹配
i=7 abababa|abbbb
j=5   ababa|bbab

// 于是再次把j调整为next[j]=next[5]=3
i=7 abababa|abbbb
j=3     aba|babbab

// 调整后依旧不匹配
i=7 abababa|abbbb
j=3     aba|babbab

// 继续调整j为next[j]=next[3]=1
i=7 abababa|abbbb
j=1       a|bababbab

// 仍然不匹配,那就再继续调整为next[j]=-1。
i=8 abababaa|bbbb
j=-1 [-1了,请看下面]

// 因为-1代表着匹配不到,所以这时只能按照暴力的i++,j++(-1自增为0),匹配s[9]和p[0]
i=9 abababaab|bbb
j=0          |abababbab

// s[9]和p[0]不匹配,好吧,继续调整j->next[j]=next[0]=-1
i=9 abababaab|bbb
j=-1

// 到-1匹配不到了,继续i++,j++
i=10 abababaabb|bb
j=0            |abababbab

// 依旧调整j=next[j],因为到-1了,所以再i++,j++,
i=11 abababaabbb|b
j=0             |abababbab

// 老规矩调整直到i=len(s)结束匹配,结果为未找到。
i=12 abababaabbbb|
j=0              |abababbab


next数组

在这个过程中,我们发现每次j变成next[j]后,'|'左边的若干个字符总是匹配好的(-1除外),这是next数组的值导致,那么next数组到底是什么?

next[ i ]:以p[i-1]为后缀的字符串 与 p串自身的前缀的 最长公共串长度(可以有重叠部分)。当然也可以理解为要替换时的下标(若字符串下标从0开始)。

可以再看下之前"abababbab"的next数组。

下标012345678
字符abababbab
next[i]-100123401

next[1]:这个特殊,肯定为0

next[2]:s[0...1]里怎么找都没有前后缀相等,为0

next[4]:s[0..1]和s[2..3]

next[6]:s[0..3]和s[2..5]

那么为什么next数组是前缀后缀公共序列长度呢?

假设匹配到s[i],p[j] 时失配,为了达到最可能把 j 移动长一点,我们让调整后s[loc...i-1]和p[0...m]相等,而s[loc...i-1]又等于p[loc...j-1](匹配到i,j了则前面的肯定相同),所以问题转为求让p[0...m]和p[loc...j-1]相同的loc,也就是next[j]

i=7 abababa|abbbb
j=5   ababa|bbab

// 于是再次把j调整为next[j]=3
i=7 abababa|abbbb
j=3     aba|babbab


代码怎么求呢?next数组只与模式串p本身有关,暴力求肯定慢成狗,既然是KMP字符串匹配,那不如p跟自身匹配?根据next[1...i-1]求next[i]


大概酱紫:next[0]为-1,next[1]为0,然后让p[1]和p[0]比较若相同则next[2]=1(即以1号为结尾的字符串的最长前缀匹配长度为1),下一个去比较p[2]和p[1],若相同则得出next[3]=2,继续比较p[3]和p[2];若不同则比较p[2]和p[ next[1] ]即p[0] (这个其实就是匹配失败的话重头再来)。就这样一个在前一个在后得比较下去


跟原串s和p匹配过程一样,p既当原串也当模式串,在下面的过程分析中还是用i,j,求的是next[i](在上面的s,p匹配中我们知道原串肯定会扫一遍),而j拿来跳转。

对于第二个字符串,能在 | 左边的肯定是因为s[i-1]和s[j-1]相同或者从-1自增过来的,那么第二个字符串的长度就是前后缀匹配长度 next[i] 了

//初始化,i永远走在前面~
next[0] = -1;
i = 0, j = -1;

//Begin
i=0  |abababbab
j=-1 abababbab
// 因为j等于-1,跟之前一样只能暴力i++,j++,得i=1,j=0,[next[1] = 0];
i=1  a|bababbab
j=0   |abababbab

// p[i]和p[j]不等,不等怎办,调整j到next[j]呗,调整j为next[0] = -1;
i=1   a|bababbab
j=-1  
// 到j=-1了,暴力ij++,得[next[2] = 0];

i=2   ab|ababbab
j=0     |abababbab
// p[2]==p[0],自然i++,j++去匹配下一个,得[next[3] = 1];
i=3   aba|babbab
j=1     a|bababbab
// 同理next[4]=2;

//  后续省略

代码

#include <cstdio>  
#include <cstring>  
  
const int maxn = 1e5 + 5;  


int* getNext(char *s2){
	int len1 = strlen(s2);
	int* next = new int [len1];
	next[0] = -1;
	int i = 0, j = -1;

	while(i < len1){
		if(j == -1 || s2[i] == s2[j]) next[++i] = ++j;
		else j = next[j];
	}
	return next;
}
//返回s1中s2首次出现的位置
int kmp(char* s1,char* s2){
	int len1 = strlen(s1);
	int len2 = strlen(s2);
	int *next = getNext(s2);
	int i = 0,j = 0;
	while(i < len1){
		if(j == -1 || s1[i] == s2[j]) i++,j++;
		else  j = next[j];
		if(j == len2){
			return i - len2;
		}
	}
	return -1;

}

char s1[maxn], s2[maxn];  
int main() {  
    // freopen("in.txt","r",stdin);  
    while (scanf("%s%s", s1, s2) != EOF) {  
        printf("%d\n", kmp(s1, s2));  
    }  
    return 0;  
}  



下面是next数组验证程序

#include <iostream>
#include <algorithm>
#include <cstdio>
#include <string.h>
#include <string>
using namespace std;

const int maxn=111111;

int next[maxn];

void getNext(string s2,int *next)
{
    next[0]=-1;
    int i=0,j=-1;
    int len=s2.length();
    while(i<len){
        if(j==-1 || s2[i]==s2[j]){
            next[++i]=++j;
        }else{
            j=next[j];
        }
    }
}

int main()
{
    string s1;
    // freopen("1.txt","r",stdin);
    while(cin>>s1){
        getNext(s1,next);
        cout<<" ";
        for (int i = 0; i < s1.length(); ++i) {
            cout<<s1[i]<<" ";
        }
        cout<<endl;
        for (int i = 0; i < s1.length(); ++i) {
            cout<<next[i]<<" ";
        }
        cout<<endl<<endl;
    }
    return 0;
}

HDU和POJ的一些KMP题目链接

2015-7-29 22:00:24

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值