KMP算法-字符串匹配问题

【题目】

给定俩个字符串str和match,长度分别为N和M。实现一个算法,如果字符串str中含有字串match,则返回match在str中的开始位置,不含有则返回-1;

【举例】

str="acbc",match="bc",返回2.

str="acbc",match="bcc",返回-1.

【解答】

 

  • 普通解法
    从左到右遍历str的每一个字符。然后看如果以当前字符作为第一个字符出发是否匹配出match。
    例子:str="aaaaaaaaaaaaaaaab"(16个a,一个b),match="aaaab"(4个a,一个b)
    从str[0]出发,开始匹配,匹配到str[4]=='a'时发现和match[4]=='b'不匹配。所以匹配失败。
    结论:说明从str[0]开始是不行的。
    然后再从str[1]开始匹配,匹配到str[5]=='a'时发现和match[4]='b'不匹配。所以匹配失败。
    结论:说明从str[1]开始是不行的。
    ...
    从str[2..12]出发都会失败。
    从str[13]开始,匹配到str[17]=='b'时发现和match[4]=='b'一样,且match已经全部匹配完,说明匹配成功,返回开始匹配的位置13.

    总结:普通解法的时间复杂度较高,从str每个字符出发时,匹配的代价都可能是O(M)  (M为match的长度),str一共有N个字符,所以整体的时间复杂度为O(M*N).普通解法的时间复杂度之所以这么高,是因为每次遍历到一个字符时,检查工作相当于从来都没有开始过(意思为不能很好的利用已经检查过的部分。即之前的检查结果虽然失败但是能有效的利用导致的。
  • KMP算法
    该算法时由Donald Knuth,Vaughan Pratt和James H.Morris于1977年联合发表的。
    该算法主要是为了高效的解决字符串的匹配问题而诞生的一个算法。
    该算法的必要准备工作:

    1,求匹配字串match的nextArr数组.
    该数组的定义为:nextArr[i]的含义是在match[i]之前的字符串match[0..i-1]中,
    必须以match[i-1]结尾的后缀子串(不能包含match[0])于
    必须以match[0]开头的前缀子串(不能包含match[i-1])最大匹配长度是多少。
    这个长度就是nextArr[i]的值。

    举例:match="aaaab" 那么nextArr[4]的值是多少呢?
    根据定义match[4]=='b'的之前的子串"aaaa"的后缀子串和前缀子串最大匹配为"aaa".也就是match[0..2]和match[1..3]
    所以nextArr[4]==3.

    match="abc1abc1" 那么nextArr[7]的值是多少呢?
    根据定义match[7]=='1'前面的子串的 前缀子串和后缀子串的最大匹配为"abc"。也即是match[0..2]和match[4..6]
    所以nextArr[7]==3

    那么nextArr数组有什么作用呢?我们后面会讲到!接下来先看KMP算法的原理:
    2,KMP算法的匹配规则
  • 如上图,假设从str[i]开始匹配,且str[i]==match[0],str[i..j-1]==match[0..j-i-1]。只有匹配到str[j]时match[j-i]!=str[j],匹配停止。因为我们已经有了match的nextArr数组那么我们接下来的匹配就无需再从str[i+1]开始匹配match[0]这样的操作了。

我们知道在match中a区域与b区域是匹配的,所以下次匹配只需要将match[k]和str[j]进行比较即可。之后一直进行这样的滑动匹配过程,直到在str的某一个位置把match完全匹配完,就说明str中有match。如果滑动到最后也没有匹配出来,就说明str中没有match。


 验证匹配算法的正确性:

1,上述中,为什么要直接让match[k]和str[j]直接比较????

 如上图,假设匹配到A字符和B字符才发生了不匹配,所以c区域必然等于b区域,又因为我们直到nextArr的定义,所以我们也直到b区域也等于a区域,那么我们就可以直接将match右滑动到如图所示的位置,直接让C字符和A字符比较。
2,为什么a区域和b区域直接肯定匹配不了match呢???
我们用假设法:假设BB区域开始位置是不用检查的其中一个位置,如果从这个位置可以匹配出match部分子串,那么,从match[0]开始也必然存在和BB区域相等的区域AA可以匹配出match部分子串。同时我们注意到AA比a区域大,BB比b区域大,我们找到了比b和a更大的前缀和后缀子串。这个结论和求nextArr的值是相互矛盾的。

 匹配过程分析完毕:match一直向右滑动,最坏的时间复杂度为O(N).

代码:

int getIndexof(const char *strContent, const char *match)
{
	if (strContent == NULL || match == NULL || strlen(strContent) < 1 || strlen(match) < 1)
	{
		return -1;
	}
	int si		= 0;
	int mi		= 0;
	int *next	= getNextArray(mi);
	while (si < strlen(strContent) && mi < strlen(match))
	{
		if (strContent[si] == match[mi])
		{
			si++;
			mi++;
		}
		else if (next[mi] == -1)
		{
			si++;
		}
		else
		{
			mi = next[mi];
		}
	}
        free(next);
        if(next != NULL)
            next = NULL;
	return mi == strlen(match) ? si - mi : -1;
}

最后一步求得nextArr数组:

1,对于match[0]而言,它前面没有字符,所以nextArr[0]规定为-1.对于match[1]来说,在它之前只有match[0],但是nextArr的定义是要求任何子串的后缀不包括第一个match[0],故nextArr[1]==0.
2,之后的nextArr数组的值求解过程如下:

  • 求nextArr数组的值因为是从左向右求,所以在求解nextArr[i]的时候nextArr[0..i-1]已经求得.
    如上图已经知道了nextArr[i-1]的值,那么求nextArr[i]只需要判断C是否等于B,如果等于那么nextArr[i]=nextArr[i-1]+1.

  • 如果不等于那么:假设C的下标为cn,cn的值就是nextArr[i-1]的值。nextArr[cn]的值就是n区域和m区域的大小
    现在只需要判断D是否等于B,如果等于那么nextArr[i]=nextArr[cn]+1;如果不等于接着按照上面步骤判断,直到cn
    等于0为止。

代码:

void getNextArray(const char * match, int *next)
{
	if (match == NULL || next == NULL)
		return;
	int length = strlen(match);
        next = (int *)malloc(sizeof(int) * length);
        if(next==NULL)
        {
            return ;
        }
	if (length == 1)
	{
		next[0] = -1;
		return;
	}
	
	next[0] = -1;
	next[1] = 0;
	int pos = 2;//从下标2开始
	int cn = 0;//
	while (pos < length)
	{
		if (match[pos - 1] == match[cn])
		{
			next[pos++] = ++cn;
		}
		else if (cn > 0)
		{
			cn = next[cn];
		}
		else
		{
			next[pos++] = 0;
		}
	}
}

整个KMP算法的复杂度为O(M)(求解nextArr数组的过程)+O(N)(匹配的过程),因为有N>=M所以时间复杂度为O(N). 

猜你喜欢

转载自blog.csdn.net/qq_42418668/article/details/89398050