字符串匹配算法--朴素匹配和KMP算法

字符串匹配:是输入源串与子串,在源串中寻找子串,并在源串中返回子串在源串中第一次出现的位置。

对此问题,我们提供两种方法:朴素匹配和KMP算法。我们假设源串长度为n,子串长度为m

朴素匹配:这个方法也称为暴力匹配,因为他就是我们最容易想到的一种字符串匹配的算法。就是从源字符串开始搜索,若出现不能匹配,则从原搜索位置+1进行搜索。

那么它的时间复杂度就需要O(n*m),可以看出时间复杂度很高。我们来看看代码把

int Simple(const char *father,const char *child)
{
	int n=strlen(father);
	int m=strlen(child);
	int i;
	for(int j=0;j<=n-m;j++)  //j<=n-m,因为当j>n-m时意味着父串的有效长度没有子串长,那就没有比较的意义了
	{
		for(i=0;i<m;i++)   
		{
			if(father[j+i]!=child[i])
				break;
		}
		if(i==m)
		{
			return j;  //匹配到了,返回源串的下标
		}
	}
	return -1;   //未匹配到返回-1
}

为了提高算法的效率,我们提出KMP算法,我们来看看

KMP算法:KMP算法的关键是利用匹配失败后的信息,尽量减少子串与主串的匹配次数以达到快速匹配的目的。时间复杂度O(m+n)。

具体思想我们用一个例子来说明把

设主串(下文中我们称作T)为:c a b a c a a b a c b a b a c a b a a b a c a b

模式串(下文中我们称作W)为:a b a c a b

1:第一次我们将主串的第一个字符与模式串的第一个字符相比较,发现a与c不同,将模式串向后移一位。

《字符串匹配算法--朴素匹配和KMP算法》

2:我们再比较主串第二位与模式串第二位,发现依旧相同,第三位也相同,第四位。。。。我们一直寻找,直到找到与模式串不同的位置。我们发现在主串的第7位(从1开始数)不同,那么我们下次应该再从哪里开始匹配呢?我们用肉眼一看,就应该吧模式串向后移动4个位置,即主串的第6位(a)与模式串的第一位对应。因为我们分明可以看出中间那些刚才匹配的字符串并不能匹配我们模式串,因为我们在遍历的时候,没有遍历到的元素我们假装是未知的,所以我们应该将模式串往后移动4位,虽然模式串的最后一位b与主串的第7位并不对应,但是不代表我们主串的第7位与模式串的第2位不对应。所以我们没有必要像朴素匹配那样将模式串一次移动一个位置。那么我们应该如果获取这个移动的距离呢?我们就应该利用好我们刚才获取的所匹配的信息来进行计算。

《字符串匹配算法--朴素匹配和KMP算法》

3:对于上面说的移动距离,我们先给大家提供一个<<匹配表>>(当然这个是我们自己算出来的,下文一会会介绍给大家,大家先看看怎么使用吧)。

我们可以看出模式串中的b与主串的a不相同,但是前5个字符匹配,我们查表可得,最后一个匹配的字符a对应的部分匹配值为3,因此按照下面的公式算出向后移动的位数:移动位数 = 已匹配的字符数 – 对应的匹配值

所以移动位数=5-1=4;所以将模式串向后移动两位,即上图所示。

4:这时候再看b与a不匹配,模式串还得往后移,那么这时的以匹配数只有一个a,对应的匹配数是0,所以移动位数=1-0=1;往后移动一位。

《字符串匹配算法--朴素匹配和KMP算法》

5:a与a对应,b与b对应。。。。直到主串11位与模式串第5位不同,我们经过查表计算,将子串往后移动四位

《字符串匹配算法--朴素匹配和KMP算法》

6:继续比较,发现不同,往后移动一位,发现完全匹配上了。于是搜索完成,如果要将全部匹配找出,我们仍然像以上方法那样做就行了。

省略了一副只移动了一格的图。。。

《字符串匹配算法--朴素匹配和KMP算法》 好了,以上就是如何移动模式串的方法了,接下来我们就来看一下如何计算这个匹配图吧

实际上我们移动的方式就是在已经匹配的字符串中,尽可能的寻找最大的相同的子串。但是这个子串一个是前缀,一个是后缀,来进行匹配。最后移动就可以将模式串移动到匹配中的后缀中的最大子串的首位,这样就保证了模式串移动距离最大。

对于”前缀”和”后缀”我们做个简单介绍(目的就是为了找到最大的已匹配串中的最大子串)。 “前缀”指除了最后一个字符以外,一个字符串的全部头部组合;”后缀”指除了第一个字符以外,一个字符串的全部尾部组合.(我们操作的都是已经匹配的字符串哦!)

“匹配值”就是”前缀”和”后缀”的最长的共有元素的长度。我们来举个栗子,以”abacab”为例,

  •  ”a”的前缀和后缀都为空集,共有元素的长度为0;
  • ”ab”的前缀为[a],后缀为[b],共有元素的长度为0;
  • “aba”的前缀为[a, ab],后缀为[ba, a],共有元素的长度1;
  • ”abac”的前缀为[a, ab,aba],后缀为[bac, ac,c],共有元素的长度0;

  • ”abaca”的前缀为[a,ab,aba,abac],后缀为[baca,aca,ca,a],共有元素的长度为1;

  • ”abacab”的前缀为[a,ab,aba,abac,abaca],后缀为[bacab,acab,cab,ab,b],共有元素为”A”,长度为2;

我们这样匹配的目的是,有时候,字符串头部和尾部会有重复。比如,”abacab”之中有两个”ab”,那么它的”匹配值”就是2(”AB”的长度)。搜索词移动的时候,第一个”ab”向后移动4位(字符串长度-部分匹配值),就可以来到第二个”ab”的位置。实际上我们从已匹配的字符串中,从后往前搜索,从前往后搜索,搜索到相同的最长的子串即是我们模式串最终要往后移动的位置。

我们来看看代码吧:

#include<stdio.h>
#include<string.h>
#include<malloc.h>
#include<assert.h>

void GetNext(const char *child,int *next)   //匹配数组
{
	int len=strlen(child);
	next[0]=-1;  //0号位置失配,前后都没有元素
	next[1]=0;   //1号位置适配,前面只有一个元素,不存在前缀或者后缀

	int k=0;    //记录模式串中的前缀最后一个元素的下标
	int j = 1; //通过j求j+1的值
	while(j+1<len)
	{
		if(k==-1 || child[j]==child[k])
		{
			next[++j] = ++k;
		}
		else
		{
			k = next[k];   
		}
	}
}

int KMP(const char *father,const char *child)
{
	int lenf=strlen(father);
	int lenc=strlen(child);
	if(lenc>lenf)
		return -1;
	int *next=(int *)malloc(sizeof(int)*lenc);
	GetNext(child,next);
	int i=0;
	int j=0;
	int count=0;
	while(i<lenf&&j<lenc)
	{
		if(father[i]==child[j])
		{
			i++;
			j++;
			count=j;
		}
		else
		{
			if(j==0)
				i++;
			else
				j=next[j];
		}
	}
	if(j==lenc)
		return i-lenc;
	if(i==lenf)
		return -1;
}

void main()
{
	char *father="cabacaabacbabacabaabacab";
	char *child="abacab";
	printf("%d\n",KMP(father,child));	
}

 

  

 

    原文作者:KMP算法
    原文地址: https://blog.csdn.net/Eunice_fan1207/article/details/82747086
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞