字符串 kmp算法解析

kmp算法是用于解析字符串匹配的问题。给定两个字符串:第一个是文本串str,第二个是匹配串p。问str中最早有那个位置能完全和匹配串p匹配呢?

1. 暴力匹配

假设文本串的长度为n,匹配串的长度为m. 那么显然有一种暴力解法是:

int match(string str, string p){
	int i = 0;
	int j = 0;
	while (i < str.size() && j < p.size())
	{
		if (str[i] == p[j]){
			i++;
			j++;
		}
		else{
			i = i - (j - 1);
			j = 0;
		}
	}
	if (j == p.size()){
		return i - j;
	}
	else{
		return -1;
	}
}

显然它的复杂度是O(m*n)。因为对于失配的情况,j每次都要退回到0位置,i也要退回到之前匹配成功的下一个字符。然后重新进行匹配。显然在进行每个字符的匹配的时候,str和p的每个字符的信息是经过记录的,而这个信息在暴力匹配的时候是没有用到的。那么有没有一种办法能用到这个信息,让str只进行one loop一次循环呢?答案就是kmp算法。

2. kmp概述

我们举个例子来解析一下kmp算法的优势:

比如str = AABBAACAABBAAC 而匹配字符串是p=AABBAAB. 当我们分别对两个字符串的头进行匹配,一直进行下去知道str[6] == 'C' p[6] == 'B'. 这个时候发现失配了。从传统的暴力法来说,那么应该让p的头字符A重新和str的第二个字符A进行匹配。但是,我们发现匹配串AABBAAB上面是有规律的。最后一个B前面的AABBAA是一个“对称串”,对于这个B之前的字符串,它的前两个字符AA和后两个字符AA是一样的。而这个时候str和p之前已经匹配的部分(AABBAA)对于AA已经是匹配好的,所以p不需要直接移动到位置0重新匹配,对于串p, 直接左移两位,让str 的AABBAACp的AABBAAB匹配,再进行下面的步骤就行。

也就是说,重点回到了寻找匹配串p中前缀后缀最长公共长度。假设p有一个对应的这个长度数组next[], p[j]字符对应的长度是next[j],表示j位置之前的前缀后缀最长公共长度是next[j]. 有了这个长度,当发生失配的时候,p匹配串只需要右移动j-next[j]就可以,而不是简单的j回到0(也就是右移j).

3. next数组

比如说字符串p= abab 那么它对应的最长前缀后缀公共串长度就是[0, 0, 1, 2](这是包含了本字符)。求next数组的时候,因为是不包含的,所以需要各个位置在之前的基础上左移一位,然后0位置赋值为-1.所以next数组是[-1, 0, 0 1]

那么怎么根绝next数组进行匹配呢?

当匹配失配的时候,令j = next [j],模式串向右移动的位数为:j - next[j]。换言之,当模式串的后缀pj-k pj-k+1, ..., pj-1 跟文本串si-k si-k+1, ..., si-1匹配成功,但pj 跟si匹配失败时,因为next[j] = k,相当于在不包含pj的模式串中有最大长度为k 的相同前缀后缀,即p0 p1 ...pk-1 = pj-k pj-k+1...pj-1,故令j = next[j],从而让模式串右移j - next[j] 位,使得模式串的前缀p0 p1, ..., pk-1对应着文本串 si-k si-k+1, ..., si-1,而后让pk 跟si 继续匹配。
如下图:

那么问题就来了,如何求next呢?求next数组需要递推,也就是说对于值k,已有p0 p1, ..., pk-1 = pj-k pj-k+1, ..., pj-1,相当于next[j] = k,那么怎么求next[j+1]呢?

4. next数组求法

算法如下:

1. 如果当 p[k] == p[j]时候,则next[j + 1 ] = next [j] + 1 = k + 1

2. 如果 p[k ] != p[j],如果此时p[ next[k] ] == p[j ],则next[ j + 1 ] =  next[k] + 1,否则继续递归前缀索引k = next[k],而后重复此过程。 

上面的1很好理解。

当p[j] == p[k]的时候,因为p[j]前面的k个字符是前缀后缀都相同的(上图大括号下面的两个k长度的部分),那么如果p[j]==p[k]则在原来的基础上左右分别添上了一个pk和pj。对于p[j+1]向左看去,相当于左边有k+1和前缀后缀相等的部分。

最难理解的是p[j] != p[k]的时候。因为当p[j] != p[k]的时候,p0~pk和pj-k ~pk组成的两个字符串是不同的。那么我们需要找一个尽可能长的m,使得p0~pm ==pj-m~pj。我们看上图的左半部分:当pk~=pj的时候,对于这个k,它的next[k]表示第k个字符左边前缀后缀相等的部分(左半部分黄色左边的蓝色)。而这一部分,因为之前说了两个大括号下面的k个都是对称的,那么在右半部分的右边的蓝色一定也是存在一个和P0~Pnext[k]相同的部分。如果再加上pnext[k]和p[j]匹配,那么就说明又找到最长的;否则k=next[k]接着递归寻找。

所以求next数组的步骤如下:

void getNext(string str, vector<int> &next){
	next.push_back(-1);
	int k = -1;
	for (int j = 1; j < str.size(); j++){
		while (k != -1 && str[j - 1] != str[k])
		{
			k = next[k];
		}
		k++;
		next.push_back(k);
	}
}

5. 根据next数组写出kmp算法

已知了next数组,kmp算法如下:

如果文本串str匹配到 i 位置,模式串p匹配到 j 位置
        当j = -1,或者当前字符匹配成功(str[i] == p[j]),那么i++,j++,也就是可以匹配下一个字符;
        当j != -1,且当前字符匹配失败(str[i] != p[j]),那么i 不变,j = next[j]。也就是当失配时,匹配串p相对于文本串str向右移动了j - next [j] 位。
        换言之,当匹配失败时,模式串向右移动的位数为:失配字符所在位置 - 失配字符对应的next 值,即移动的实际位数为:j - next[j],且此值大于等于1。

代码如下:

int kmp(string str, string p, vector<int> &next){
	//str1是待匹配文本串,p是匹配字符串,next是匹配串p对应的next数组
	//i j分别表示匹配到str1和p的i,j位
	int i = 0;
	int j = 0;
	while (i < str.size() && j < (int)p.size())
	{
		if (j == -1 || str[i] == p[j]){
			i++;
			j++;
		}
		else{
			j = next[j];
		}
	}
	if (j == p.size()) return i - j;
	else return -1;//-1表示无匹配
}

6. kmp算法复杂度分析:

首先我们先看计算next数组的复杂度:O(m)

再看kmp算法:我们发现如果某个字符匹配成功,模式串首字符的位置保持不动,仅仅是i++、j++;如果匹配失配,i 不变(即 i 不回溯),模式串会跳过匹配过的next [j]个字符。整个算法最坏的情况是,当模式串首字符位于i - j的位置时才匹配成功,算法结束。
    所以,如果文本串的长度为n,模式串的长度为m,那么匹配过程的时间复杂度为O(n),算上计算next的O(m)时间,KMP的整体时间复杂度为O(m + n)。

 

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值