计算子串在主串中的位置及其优化(KMP算法)

问题描述:设置一个起始位置,寻找主串中第一次出现子串的首位置。

算法实现:

int index(string str,string substr,int pos)
{
	int i=0,j=0;
	int slen,sslen;

	i=pos;
	slen=str.length();
	sslen=substr.length();

	while(i+sslen<slen)
	{
		while(j<sslen)
		{
			if(str[i+j]==substr[j])
				j++;
			else
				break;
		}
		if(j==sslen)
			return i+1;
		else{
			j=0;
			i++;
		}
	}
	return 0;
}

循环开始,判断当前位置加上子串的长度是否超出主串的长度,若没有超出,开始比较主串当前位置字符与子串字符,如果主串与子串的字符相等,则继续比较直到子串末尾,然后返回子串在主串中的位置;若出现子串与主串不相等的情况,则跳出比较的循环,子串位置归零,主串位置后移一位,继续比较。

这种方法好理解,但是会产生很多不必要的计算。

比如主串是abcdefabcdexghij,子串是abcdex,假设我们从主串起始位置开始比较。

比较到第六个字符时候发现不同,然后主串向后移动一位继续与子串比较,发现第一个就不同,然后主串继续后移一位,这样一直比较到第七个字符串。其实发现前面从主串的第二个字符串开始到第五个的比较是没有什么用处的,完全是多余的运算,这里保留第六个是因为此处出现了不同,无法判定和起始位置的字符就不同。

能不能有种算法避免这种情况,有一种算法叫做KMP,可以实现避免这种状况。

首先需要计算子串的变化数组称为next数组。

这个next数组怎么计算呢。j是子串的标记位。

当j为1的时候,也就是在子串首位的时候next[j]=0;

当子串向后移动j个位置,出现重复的字符串的首位,这里可能会有多个重复的字符,这里取最大的一个,如abcabx,当j=5的时候,前面的abca,第四个字符与第一个字符重复,则next[5]=1+1=2。当j=6的时候,前面的abcab字符串中,首位ab与末尾ab相重复,则去最大的next[6]=2+1=3。

如果没有重复,则next数组为1.

上面的描述用公式表示出来如下:

《计算子串在主串中的位置及其优化(KMP算法)》

这样对于“abcabx”这样的子串的next数组可以如下表示:

j:          1     2     3    4    5     6   

S:        a     b     c    a     b      x

next:   0     1     1    1    2      3

获取next数组的源代码实现如下:

void get_next(int next[],string str)
{
	int i=0,j=-1;
	next[0]=-1;
	while(str[i]!='\0')
	{
		if(j==-1 || str[i]==str[j])
		{
			++i;
			++j;
			next[i]=j;
		}
		else
			j=next[j];
	}
}

这里需要说明的一点是,i是指向字符串尾部的量,j是指向字符串头部的量。

开始我们初始化i=0,j=-1,next[0]=-1。尾部在首,则头部在数轴另一侧;

起始的时候j=-1,字符无法比较,但是满足j==-1的条件,进入条件体内部,i向前移动一位,j也向前移动一位,计算出next[1]=0。然后下次不满足j==-1的条件,再看str[1]==str[0]是否成立,如果不成立,那么j复位为next[0],满足条件j==-1,i,j继续向前移动,此时i=2,j=0。如果成立,那么i,j均向前移动一位,这种条件下i=2,j=1.按照这样的规律进行移动,最终完成next数组的构建。

则结合next数组实现的KMP算法如下:

int kmp(string str,string substr,int pos)
{
	int next[256]={0};
	get_next(next,substr);
	int i=pos,j=0;

	while(i<str.length() && j<substr.length())
	{
		if(j==-1||str[i]==substr[j])
		{
			++i;
			++j;
		}
		else 
			j=next[j];
	}

	if(j==substr.length())
		return (i-j);
	else
		return 0;
}

上面的kmp算法虽然能过将普通的查找子串的复杂度,从O((n-m+1)*m)降低到O(m+n)。

但是KMP有缺陷,有种极端的情况比如主串是“aaaabaaaaab”。子串是“aaaaab”。

从子串可以得出next数组为0,1,2,3,4,5。当进行匹配的时候,发现到第五个字符失配,按照next数组,这时候j回溯一位,又发现不同再回溯,一直到子串的首位,其实可以发现回溯到首位之前的这些操作完全是没用的。所以我们发现将首位的字符的next值去替换与它相等的后续字符的值可以极大提高算法的效率。对get_next函数进行改进,可以得到下面的函数:

void get_next(int next_new[],string str)
{
	int i=0,j=-1;
	next_new[0]=-1;
	while(str[i]!='\0')
	{
		if(j==-1 || str[i]==str[j])
		{
			++i;
			++j;
			if(str[i]!=str[j])
			     next_new[i]=j;
			else
			      next_new[i]=next_new[j];
		}
		else
			j=next_new[j];
	}
}

这样对于“abcabx”这样的子串的next数组可以如下表示:

j:          1     2     3     4      5     6   
S:          a     b     c     a      b     x
next:       0     1     1     1      2     3
next_new:   0     1     1     0      1     3

再看一个子串例子,进行深入理解,我们根据计算next数组的方法进行计算,然后在next数组的基础上进行一点小小的修改形成最终的next数组:

j:         1     2     3    4    5     6    7    8    9   
S:         a     b     a    b    a     a    a    b    a
next:      0     1     1    2    3     4    2    2    3
next_new:  0     1     0    1    0     4    2    1    0

上面计算KMP的next数组我的理解如下:

先计算中间过程next,再根据next计算next_new。

我以这个”ababaaaba”这个为例。

当j=1时,next[1]=0;

当j=2时,next[2]=1;

当j=3时,next[3]=1;

当j=4时,前面的“aba”出现了重复,从起始到结束的最大重复子串为“a”,其位置为k-1=1,则next[4]=k=2;

当j=5时,前面继续出现重复,“abab”,最大重复子串为“ab”,则next[5]=k=2+1=3;

当j=6时,前面子串为“ababa”,最大重复子串为’aba’,则next[6]=k=3+1=4;

当j=7时,前面子串为“ababaa”,最大重复子串为“a”,则next[7]=k=1+1=2;

当j=8时,前面子串为“ababaaa”,最大重复子串为“a”,则next[8]=k=1+1=2;

当j=9时,前面子串为“ababaaab”,最大重复子串为“ab”,则next[9]=k=2+1=3.

通过上面,我们完整计算了中间过程next的值,接下来,以每个next所标示的字符与S中相比较,相同则next的值取前面一个字符所对应next的值,不同则保持不变,这里举个例子,比如当j=6的时候,next[6]=4,4所对应的字符为“b”,而6所对应的字符为“a”,两者不同,保持不变。

以上就是对字符串查找匹配以及KMP算法的一些理解,还不是很到位,需要加强。下面是我整个测试的代码:

#include<iostream>
#include<vector>
#include<string>
#include<queue>
#include<set>
#include<algorithm>
using namespace std;
void get_next(int next[],string str)
{
	int i=0,j=-1;
	next[0]=-1;
	while(str[i]!='\0')
	{
		if(j==-1 || str[i]==str[j])
		{
			++i;
			++j;
			if(str[i]!=str[j])
			     next[i]=j;
			else
				 next[i]=next[j];
		}
		else
			j=next[j];
	}
}
int kmp(string str,string substr,int pos)
{
	int next[256]={0};
	get_next(next,substr);
	int i=pos,j=0;

	while(i<str.length() && j<substr.length())
	{
		if(j==-1||str[i]==substr[j])
		{
			++i;
			++j;
		}
		else 
			j=next[j];
	}

	if(j==substr.length())
		return (i-j);
	else
		return 0;
}
int index(string str,string substr,int pos)
{
	int i=0,j=0;
	int slen,sslen;

	i=pos;
	slen=str.length();
	sslen=substr.length();

	while(i+sslen<slen)
	{
		while(j<sslen)
		{
			if(str[i+j]==substr[j])
				++j;
			else
				break;
		}
		if(j==sslen)
			return (i+1);
		else{
			j=0;
			++i;
		}
	}
	return 0;
}
int main()
{
	int position=0,pos=0;
	string s1,s2;
	int next[255]={0},i=0;
	cout<<"postition:"<<endl;
	cin>>pos;
	cin>>s1;
	cin>>s2;
	cout<<"next array is:"<<endl;
	get_next(next,s2);
	for(i=0;i<s2.length();i++)
	cout<<next[i]<<" ";
	cout<<endl;
	//position=index(s1,s2,pos);
	position=kmp(s1,s2,pos);
	if(position!=0)
	     cout<<"from "<<pos<<"st character:"<<position<<endl;
	else
		 cout<<"can not found!"<<endl;
}
    原文作者:KMP算法
    原文地址: https://blog.csdn.net/xygl2009/article/details/47999489
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞