zl程序教程

您现在的位置是:首页 >  后端

当前栏目

KMP算法模式匹配

算法 KMP 模式匹配
2023-09-27 14:28:31 时间

转载请注明出处

http://blog.csdn.net/pony_maggie/article/details/37832707

作者:小马


在一个长串中查找一个子串是较常用的操作。各种信息检索系统,文字处理系统都少不了。本文介绍一个非常著名的KMP模式匹配算法用于子串查找。

 

先抛开KMP,正常情况一下我们会如何设计这个逻辑。一个主串S, 要在里面查找一个子串T,如果找到了返回T在S中开始的位置,否则返回-1。应该不难,需要一个辅助函数,从一个串口的指定位置,取出指定长度的子串。思想是这样:

在主串S中从开始位置,取长度和T相等的子串比罗,若相等,返回该位置的索引值,否则位置增加1, 继续上面的过程。代码很简单,如下:

 

//普通方法查找子串
//在主串s中查找t, 若找到匹配,返回索引位置(从0到len-1)
//否则返回-1
int IndexOfString(char* srcString, char* keyString)
{
	int nSrcLen = 0;
	int nKeyString = 0;
	int i = 0;
	char szTemp[1024] = {0};//假设足够大

	if ((srcString == NULL) || (keyString == NULL))
	{
		return -1;
	}

	nSrcLen = strlen(srcString);
	nKeyString = strlen(keyString);
	if (nKeyString > nSrcLen)
	{
		return -1;
	}

	while(i < (nSrcLen-nKeyString))
	{
		memset(szTemp, 0x00, sizeof(szTemp));
		SubString(srcString, szTemp, i, nKeyString);
		if (memcmp(szTemp, keyString, nKeyString) != 0)
		{
			i++;
		}
		else return i;
	}

	return -1;
}

再进一步,把辅助函数去掉,通过频繁操作下标也同样可以实现,思想跟上面查不多,只不过换成一个个字符比较,代码如下:

 

int IndexOfString1(char* srcString, char* keyString)
{
	int nSrcLen = 0;
	int nKeyLen = 0;
	int i = 0;
	int j = 0;
	char szTemp[1024] = {0};//假设足够大

	if ((srcString == NULL) || (keyString == NULL))
	{
		return -1;
	}

	nSrcLen = strlen(srcString);
	nKeyLen = strlen(keyString);
	if (nKeyLen > nSrcLen)
	{
		return -1;
	}

	while((i < nSrcLen) && (j <nKeyLen))
	{
		if (srcString[i] == keyString[j])
		{
			i++;
			j++;
		}
		else
		{
			i = i - j + 1;//主串回退到下一个位置
			j = 0; //子串重新开始
		}
	}

	if (j >= nKeyLen)
	{
		return (i - nKeyLen);//找到
	}

	return -1;
}


分析一下上面算法的时间复杂度(两个算法其实是一样的)。举个例子,主串是:

“A STRING SEARCHING EXAMPLE CONSISTINGOF SIMPLE TEXT”

子串是

“STING”

 

用上面的算法,会发现除了上面标记红色的字符比较了两次,其它都是比较一次,如果主串的长度是m, 子串的长度是n, 时间复杂度基本是O(m+n)。好像不错,效率挺高。再来看一个。主串是:

“000000000000000000000000000000000000000000000000000000000001”

子串是

“00000001”

 

在脑海里想像一下这个过程,很容易得出它的时间复杂度是O(m*n)。所以这种类似穷举的查找子串算法,时间复杂度与主串和子串的内容有很大关系,复杂度不固定。而且像上面那样的01串在计算机处理中还比较常见,所以需要更好的算法。

 

KMP(三个发明人的名字首字母)算法可以保证不论哪种情况都可以在O(m+n)的时间复杂度内完成匹配。它改进的地方是当出现比较不等时,主串中位置不回退,而是利用已经匹配的结果,将子串向右滑动尽可能远的距离后,再继续比较,看个例子:


 

在第三趟匹配中,当i=12, j=7时,字符不相等,这时不用回退到i=7,j=1重新比较,而是i不变,j变成next[j]的值就行了,上图中是3, 也就是图中第四趟的比较位置。

 

这个确实很强大,现在要解决的问题是next[j]如何计算。其实这个推导也不难,很多算法的书上都有详细的过程,我这里就不赘述了。只把结果给出来:

 

1 当j = 0时,next[j] =-1。

2 当j =1时,next[j] = 0。

3 满足1 < k < j,且p[0…k-1] =p[j-k…j-1]的最大的k, next[j] = k。

4 其它情况,next[j] = 0。

 

根据上面的逻辑,很容易写出一个计算next的函数,如下:

static void getNext(char* strSub)
{
	int j, k, temp;
	int nLen = 0;
	j = k = temp = 0;
	nLen = strlen(strSub);

	memset(g_next, 0x00, sizeof(g_next));//每次进来都清空

	for (j = 0; j < nLen; j++)
	{
		if (j == 0)
		{
			g_next[j] = -1;
		}
		else if (j == 1)
		{
			g_next[j] = 0;
		}
		else //取集合不为空时的最大值
		{
			temp = j - 1;
			for (k = temp; k > 0; k--)
			{
				if (isEqual(strSub, j, k))
				{
					g_next[j] = k;
					break;
				}
			}
			if (k == 0)//集合为空
			{
				g_next[j] = 0;
			}
		}
	}
	

}

得到next之后,整个过程如下: 以指针i和j分别表示主串和子串中正在比较的字符,若Si = Pj, 则i和j都增1,继续下一个字符的比较。否则i不变,j退到next[j]的位置再比较,若相等,再各自增1,否则j再退到next[j]。循环进行,如果中间遇到next[j]为-1的情况,此时主串要增1,子串j变为0,表示要从主串的下一个位置重新开始比较。

 

把上面的过程转化为代码:

//KMP算法查找子串
//在主串s中查找t, 若找到匹配,返回索引位置(从0到len-1)
//否则返回-1
int IndexOfStringKMP(char* srcString, char* keyString)
{
	int i = 0; 
	int j = 0;
	int nSrcLen = 0;
	int nKeyLen = 0;

	if ((srcString == NULL) || (keyString == NULL))
	{
		return -1;
	}

	nSrcLen = strlen(srcString);
	nKeyLen = strlen(keyString);
	if (nKeyLen > nSrcLen)
	{
		return -1;
	}

	getNext(keyString);//先计算next值

	while ((i < nSrcLen) && (j < nKeyLen))
	{
		if ((srcString[i] == keyString[j]) || (j == -1))
		{
			i++;
			j++;
		}
		else
		{
			j = g_next[j];
		}
	}
	if (j >= nKeyLen)
	{
		return (i - nKeyLen);//找到
	}

	return -1;
}


代码下载地址:

http://download.csdn.net/detail/pony_maggie/7630329

https://github.com/pony-maggie/StringIndex