0
点赞
收藏
分享

微信扫一扫

计算子串在主串的次数(kmp算法)

alonwang 2022-01-13 阅读 100
算法

一、问题描述

这是一道模板题,给定一个字符串A和一个字符串B,求B在A中的出现次数。A和B中的字符均为英语大写字母或者小写字母。A中不同位置出现的B可重叠。

输入格式:

输入共两行,分别是字符串A和字符串B

输出格式:

输出一个整数,表示B在A中的出现次数。

样例:

输入:

zyzyzyz

zyz

输出:

3

二、KMP算法介绍

此处参考文档为:字符串匹配的KMP算法 - 阮一峰的网络日志 (ruanyifeng.com)​​​​​​

看了好多篇的介绍,认为这篇讲的最清楚,放入此文供大家参考。

内容如下:

有一个字符串"BBC ABCDAB ABCDABCDABDE",我想知道,里面是否包含另一个字符串"ABCDABD"?许多算法可以完成这个任务,Knuth-Morris-Pratt算法(简称KMP)是最常用的之一。这个算法完成该任务不需要主串回溯,它以三个发明者命名,起头的那个K就是著名科学家Donald Knuth。

下面以该例子讲解KMP算法的具体过程。

1)

首先,字符串"BBC ABCDAB ABCDABCDABDE"的第一个字符与搜索词"ABCDABD"的第一个字符,进行比较。因为B与A不匹配,所以搜索词后移一位。 

2)

因为B与A不匹配,搜索词再往后移。

3) 

 就这样,直到字符串有一个字符,与搜索词的第一个字符相同为止。

4)

 接着比较字符串和搜索词的下一个字符,还是相同。

5)

直到字符串有一个字符,与搜索词对应的字符不相同为止。

6)

这时,最自然的反应是,将搜索词整个后移一位,再从头逐个比较。这样做虽然可行,但是效率很差,因为你要把"搜索位置"移到已经比较过的位置,重比一遍。

7)

一个基本事实是,当空格与D不匹配时,你其实知道前面六个字符是"ABCDAB"。KMP算法的想法是,设法利用这个已知信息,不要把"搜索位置"移回已经比较过的位置,继续把它向后移,这样就提高了效率。

8)

怎么做到这一点呢?可以针对搜索词,算出一张《部分匹配表》(Partial Match Table)。这张表是如何产生的,后面再介绍,这里只要会用就可以了。

9)

已知空格与D不匹配时,前面六个字符"ABCDAB"是匹配的。查表可知,最后一个匹配字符B对应的"部分匹配值"为2,因此按照下面的公式算出向后移动的位数:

因为 6 - 2 等于4,所以将搜索词向后移动4位。

10)

因为空格与C不匹配,搜索词还要继续往后移。这时,已匹配的字符数为2("AB"),对应的"部分匹配值"为0。所以,移动位数 = 2 - 0,结果为 2,于是将搜索词向后移2位。

11)

因为空格与A不匹配,继续后移一位。

12)

 逐位比较,直到发现C与D不匹配。于是,移动位数 = 6 - 2,继续将搜索词向后移动4位。

 13)

逐位比较,直到搜索词的最后一位,发现完全匹配,于是搜索完成。如果还要继续搜索(即找出全部匹配),移动位数 = 7 - 0,再将搜索词向后移动7位,这里就不再重复了。

14)

下面介绍《部分匹配表》是如何产生的。

首先,要了解两个概念:"前缀"和"后缀"。 "前缀"指除了最后一个字符以外,一个字符串的全部头部组合;"后缀"指除了第一个字符以外,一个字符串的全部尾部组合。

15)

"部分匹配值"就是"前缀"和"后缀"的最长的共有元素的长度。以"ABCDABD"为例,

 16)

"部分匹配"的实质是,有时候,字符串头部和尾部会有重复。比如,"ABCDAB"之中有两个"AB",那么它的"部分匹配值"就是2("AB"的长度)。搜索词移动的时候,第一个"AB"向后移动4位(字符串长度-部分匹配值),就可以来到第二个"AB"的位置。 

 三、KMP算法的next数组的求法(C语言实现)

1)算法描述。有两种求解方法,分别是根据前一个字符的next值求以及根据前一个字符的next值求。本文只介绍第一种。

字符串记作 p;next 数组记作 next;

约定:

下标从 1 开始算,注意,不是从 0 开始算
字符串长度 >2
1)第一个字母的 next 值置 0 (next[1] = 0),第二个字母的 next 值置 1(next[2] = 1) ;
2)从第 3 个开始,计算第 i 个位置的 next 值时,检查

p[i-1]== p[next[i-1]] ?(即这两个值是否相等)

解释:第 i 个位置的前一个位置的值(即 p[i-1],记作 m)与以 m 的 next 值(即 next[i-1])为下标的值(即 p[next[i-1]],记作 n)是否相等,(看的懵懵的也没关系,后面会有例子)

若相等,则 next[i] = next[i-1] + 1
若不等,则继续往回找,检查

p[i-1]== p[next[next[i-1]]] ?

若相等,则 next[i] = next[next[i-1]] + 1
若不等,则继续往回找,直到找到下标为 1 还不等(即字符串第一个元素),直接赋值 next[i] = 1

2)举个例子方便理解上述算法的流程。

求解:
(1)对应上面第一种求法
1)初始化

Pababaaababaa
下标123456789101112
next01


2)求下标为 3 的字符的 next 值
P[3-1] = P[2] = ‘b’;
next[3-1] = next[2] = 1 ;
P[next[3-1]] = P[1] = ‘a’;
P[3-1] != P[next[3-1]] ,但是此时已经回溯到了第一个元素,
∴ 直接P[3] = 1 ;

Pababaaababaa
下标123456789101112
next011


3)求下标为 4 的字符的 next 值
P[4-1] = P[3] = ‘a’;
next[4-1] = next[3] = 1 ;
P[next[4-1]] = P[1] = ‘a’;
P[4-1] == P[next[4-1]] ;
∴ next[4] = next[4-1] + 1 = 2 ;

Pababaaababaa
下标123456789101112
next0112


4)求下标为 5 的字符的 next 值
P[5-1] = P[4] = ‘b’;
next[5-1] = next[4] = 2 ;
P[next[5-1]] = P[2] = ‘b’;
P[5-1] == P[next[5-1]] ;
∴ next[5] = next[5-1] + 1 = 3 ;

Pababaaababaa
下标123456789101112
next01123


5)求下标为 6 的字符的 next 值
推导过程同上 => next[6] = next[6-1] + 1 = 4 ;

Pababaaababaa
下标123456789101112
next011234


6)求下标为 7 的字符的 next 值
P[7-1] = P[6] = ‘a’;
next[7-1] = next[6] = 4 ;
P[next[7-1]] = P[4] = ‘b’;
P[7-1] != P[next[7-1]] && 此时还未回到第一个,继续
next[next[7-1]] = next[4] = 2 ;
P[next[next[7-1]]] = P[2] = ‘b’;番外(1)
P[7-1] != P[next[next[7-1]]] && 但是此时还未回到第一个,继续
next[next[next[7-1]]] = next[2] = 1 ;
P[next[next[next[7-1]]]] = P[1] = ‘a’ ;
P[7-1] == P[next[next[next[7-1]]]] ;
∴ next[7-1] = next[next[next[7-1]]] + 1 = next[2] + 1 = 2 ;

Pababaaababaa
下标123456789101112
next0112342


7)求下标为 8 的字符的 next 值
P[8-1] = P[7] = ‘a’;
next[8-1] = next[7] = 2 ;
P[next[8-1]] = P[2] = ‘b’;
P[8-1] != P[next[8-1]] ,但是还没回到第一个元素,继续
next[next[8-1]] = next[2] = 1 ;
P[next[next[8-1]]] = P[1] = ‘a’;
P[8-1] == P[next[next[8-1]]];
∴ next[8] = next[next[8-1]] + 1 = 2

Pababaaababaa
下标123456789101112
next01123422


8)求下标为 9 的字符的 next 值
推导过程同4) => next[9] = next[9-1] + 1 = 3 ;

Pababaaababaa
下标123456789101112
next011234223


9)求下标为 10 的字符的 next 值
推导过程同4) => next[10] = next[10-1] + 1 = 4 ;

Pababaaababaa
下标123456789101112
next0112342234


10)求下标为 11 的字符的 next 值
推导过程同4) => next[11] = next[11-1] + 1 = 5 ;

Pababaaababaa
下标123456789101112
next01123422345


11)求下标为 12 的字符的 next 值

推导过程同4) => next[12] = next[12-1] + 1 = 6 ;

Pababaaababaa
下标123456789101112
next011234223456

3)算法实现。

//测试样例
//abaabcaba      next -1 0 0 1 1 2 0 1 2
//ababaaababaa   next  -1 0 0 1 2 3 1 1 2 3 4 5
//abaabcac       next -1 0 0 1 1 2 0 1
void getNext(char p[],int next[])
{
	int len = strlen(p);
	next[0] = -1;    //p的起始下标为0,所以这里设置为-1
	int k = -1;      //同上理由
	int j = 0;
	while(j<len-1)      
	{
		if(k == -1 || p[k] == p[j])
		{
			j++;
			k++;
			next[j] = k;
		}
		else
		{
			k = next[k];
		}
	}
}

四、解决问题

基于二三的理论,开始着手解决一板块中的问题。

该问题的解决有三种方法,如下。

基础方法,穷举法:从原串的各个位置开始与模式串进行比较,如若与模式串匹配则存在模式串,计数加1。但此时算法复杂度为O(len_src*len_ptn)。

改进方法1,KMP:首先利用KMP算法,得到模式串的next数组,然后利用next进行匹配,如若匹配成功,则返回原串中起始匹配位置pos,计数加1,之后从原串pos+1处再次利用next进行匹配,直到匹配失败。因为要计算模式串出现的次数,故此时算法的最坏复杂度仍为O(len_src*len_ptn)。

改进方法2,KMP:对改进方法1,时间复杂度仍然很高,究其原因是未充分利用next数组。每次匹配成功(原串匹配起始位置为pos)后下一次判断匹配起始位置(为pos+1)取用有问题,应该如同简单KMP方法一样,不要回溯原串索引位置,而是利用next数组,调整模式串的索引位置。设匹配成功时,原串索引起始位置为pos,则下次匹配判断的原串起始索引位置应为pos+len_ptn,而模式串的起始索引位置应为next[len_ptn]。

解决该问题需要使用改进方法2,此时,需要计算next[len_ptn],所以,getNext函数修改如下。


void getNext(char p[],int next[])
{
	int len = strlen(p);
	next[0] = -1;
	int k = -1;
	int j = 0;
	while(j<len)    //get_next函数进行升级的点:这里不是j<len-1 而是j<len,这样能够不回溯计数  
	{
		if(k == -1 || p[k] == p[j])
		{
			j++;
			k++;
			next[j] = k;
		}
		else
		{
			k = next[k];
		}
	}
}

解决问题的全部代码如下(含kmp)。

#include<stdio.h>
#include<string.h>
int next[100];
void getNext(char ptn[],int next[])
{
	int len = strlen(ptn);
	next[0] = -1;  //下标0处的初始化
	int k = -1;
	int j = 0;
	while(j<len)    //get_next函数进行升级的点:这里不是j<len-1 而是j<len,这样能够不回溯计数  
	{
		if(k == -1 || ptn[k] == ptn[j])
		{
			j++;
			k++;
			next[j] = k;
		}
		else
		{
			k = next[k];
		}
	}
}
int kmp(char src[],char ptn[])
{
	int len_src = strlen(src);
	int len_ptn = strlen(ptn);
	int i = 0,n = 0,k = 0;  //k应该从0开始,而非-1
	while(i<len_src)
	{
		if(k==-1||src[i]==ptn[k])
		{
			++i;
			++k;
		}
		else
		{
			k=next[k];
		}
		if(k==len_ptn)   //实现不回溯计数
		{
			n++;
			k = next[k];
		}
	}
	return n;
}
int main()
{
	char src[100000],ptn[10000];  
	scanf("%s %s",&src,&ptn);
	getNext(ptn,next);
	printf("%d",kmp(src,ptn));
 } 

五、参考博客

​​​​​​(38条消息) KMP:计算模式串出现的次数_no crying out, but Striving for!-CSDN博客_利用kmp算法求子串在主串中出现的次数

​​​​​​(38条消息) next数组两种求法_粉刷匠-CSDN博客_next数组求法

字符串匹配的KMP算法 - 阮一峰的网络日志 (ruanyifeng.com)

举报

相关推荐

0 条评论