算法导论-第32章- 字符串匹配 - KMP算法

书中依次讲了4种方法,朴素算法、RabinKarp算法、有限自动机算法、KMP算法

1、朴素算法:

算法用一个循环来找出所有有效位移,该循环对n-m+1个可能的每一个s值检查条件P[1...m] = T[s+1....s+m];

//朴素的字符串匹配算法
void nativeStringMatcher(string st, string sp)
{
	int n = st.length();          //主串长度
	int m = sp.length();          //模式串长度

	for (int s = 0; s <= n-m+1; s++)
	{
		string ss = st.substr(s, m);   //ss为st从s位置开始的m个字符,即和sp比较的字符串
		if (sp == ss)
			cout << "在位置" << s << "处发现匹配" << endl;
	}
}

上面函数调用了string比较是否相等的操作,如果不用这样比较,即一个字符一个字符比较,则有暴力枚举法如下:

int ViolentMatch(char* s, char* p)  
{  
	int sLen = strlen(s);  
	int pLen = strlen(p);  

	int i = 0;  
	int j = 0;  
	while (i < sLen && j < pLen)  
	{  
		if (s[i] == p[j])  
		{  
			//①如果当前字符匹配成功(即S[i] == P[j]),则i++,j++      
			i++;  
			j++;  
		}  
		else  
		{  
			//②如果失配(即S[i]! = P[j]),令i = i - (j - 1),j = 0 即j回到0,i回到i-j+1处重新比较,下面介绍的KMP算法的不同就在于i不会回到i-j+1,而j也不一定非要回到0比较     
			i = i - j + 1;  
			j = 0;  
		}  
	}  
	//匹配成功,返回模式串p在文本串s中的位置,否则返回-1  
	if (j == pLen)  
		return i - j;  
	else  
		return -1;  
} 

2、KMP算法(详细的算法说明请参看: http://blog.csdn.net/v_july_v/article/details/7041827#t5)

运行时间为O(n),预处理时间为O(m)

 举个例子,如果给定文本串S“BBC ABCDAB ABCDABCDABDE”,和模式串P“ABCDABD”,现在要拿模式串P去跟文本串S匹配,过程如下所示:

算法导论-第32章- 字符串匹配 - KMP算法_第1张图片

此时i 不是回到i-j+1 (而是不动或者+1),j 也不是回到0,下面主要的问题是求 j 回到那里。

算法导论-第32章- 字符串匹配 - KMP算法_第2张图片


由上图发现如果模式串P的D字符和文本串S的空格字符失配,则 j 不是回到0,而是回到了P的第三个字符继续与S的第 i 个字符比较。因为D之前的AB和模式串P开头的AB相同。下面代码求模式串P每个字符前的字符串的前缀串的长度。

//对于P的前j+1个序列字符(下面的k=next[j],即前j个字符的前缀串长度,下标从0到j-1):
//
//	1、若p[k] == p[j],则next[j + 1 ] = next [j] + 1 = k + 1;//前缀串增加1,比如ABABC...ABABC,k=4,p[j]=p[k]=p[4]=C 
//	2、若p[k ] ≠ p[j],如果此时p[ next[k] ] == p[j ],则next[ j + 1 ] =  next[k] + 1; //比如ABABC....ABABA,此时之前k=4(ABAB),因为p[4]!=p[j],next[k]=next[4]=2(ABAB);然后p[2]=p[j]=A,所以next[j+1]=next[k]+1=3(前缀串为ABA)
//否则继续递归前缀索引k = next[k],而后重复此过程,直到k=-1,那么next[j+1]=0。

void computePrefixFunction(char* p,int *next)  
{  
	int pLen = strlen(p);  
	next[0] = -1; //第一个字符的前缀串长度设为-1,第2个为0
	int k = -1;  
	int j = 0;  
	while (j < pLen - 1)  
	{  
		//p[k]表示前缀,p[j]表示后缀  
		//如果k=-1,则next[j]=0,即前j-1个字符没有前缀,或者如果k!= -1且p[j] == p[k],则前j-1个字符的next=k + 1(k先自增然后赋给next),然后求下一个字符的next
		if (k == -1 || p[j] == p[k])   
		{  
			++k;  
			++j;
			next[j] = k;  			 
		}  
		else   
		//如果k != -1,则把next[k]赋给k,继续找当前字符j之前的前缀串长度
		{  
			k = next[k];  
		}  
	}  
} 
结果如图所示:
算法导论-第32章- 字符串匹配 - KMP算法_第3张图片

KMP算法:

int KmpSearch(char* s, char* p)  
{  
	int i = 0;  
	int j = 0;  
	int sLen = strlen(s);  
	int pLen = strlen(p);  
	int *next = (int *)malloc(sizeof(int) * pLen);//存储p中第i个字符前的字符串的前缀串长度(比如ABCABD,当前字符为D,下标为5,则p[5]=2,"AB"="AB")
	computePrefixFunction(p,next);
	while (i < sLen && j < pLen)  
	{  
		////①如果j = -1,或者当前字符匹配成功(即S[i] == P[j]),都令i++,j++      
		////j=-1说明j = next[j],即当前字符j没有前缀串,故j要重头开始重新查找
		//if (j == -1 || s[i] == p[j])  
		//{  
			//i++;  
			//j++;  //如果j = -1 ,则此时j = 0 ,即j又从头开始判断
		//}  
		//else  
		//{  
			////②如果j != -1,且当前字符匹配失败(即S[i] != P[j]),则令 i 不变,j = next[j]      
			////next[j]即为j所对应的next值        
			//j = next[j];  //j调回到最长前缀的下一个字符,比如ABC....ABD,因为next[D]=2,则j调回到C,C再重新和i判断。比如ABC...MND,则next[D]=-1,则j=-1,下次判断后j回到0,重新判断
		//} 
		//如果当前字符相等,则i++,j++
		if(s[i] == p[j])
		{
			i++;
			j++;
		}
		else
		//否则,j退回到next[j]
		{
			j=next[j];
			//如果退回后的j为-1,说明退到了p[0],因为只有p[0]=-1,则令j=0,重头判断,并且i++,比如s="ABC",p="DEF",'A'!='D'则重新判断'B'和'D'
			//注意如果退回后的j=0,则j也退到了p[0],但是这时i不能++,因为s[i]有可能=p[0],比如说s="...ABCABCD",P="ABCD",当判断到A!=D时,j退回到了0,这是还要继续判断s[i]是否='A‘
			if(j == -1 )//j=-1,说明上个j为第1个字符,即p[0],即[0]!=p[i],那么令j=0,i则向后挪一位
			{
				j=0;
				i++;				
			}
		}

	}  
	if (j == pLen)  
		return i - j;  
	else  
		return -1;  
}

完整代码:

#include <iostream>
using namespace std;


int ViolentMatch(char* s, char* p)  
{  
	int sLen = strlen(s);  
	int pLen = strlen(p);  

	int i = 0;  
	int j = 0;  
	while (i < sLen && j < pLen)  
	{  
		if (s[i] == p[j])  
		{  
			//①如果当前字符匹配成功(即S[i] == P[j]),则i++,j++      
			i++;  
			j++;  
		}  
		else  
		{  
			//②如果失配(即S[i]! = P[j]),令i = i - (j - 1),j = 0      
			i = i - j + 1;  
			j = 0;  
		}  
	}  
	//匹配成功,返回模式串p在文本串s中的位置,否则返回-1  
	if (j == pLen)  
		return i - j;  
	else  
		return -1;  
}  
//对于P的前j+1个序列字符(下面的k=next[j],即前j个字符的前缀串长度,下标从0到j-1):
//
//	1、若p[k] == p[j],则next[j + 1 ] = next [j] + 1 = k + 1;//前缀串增加1,比如ABABC...ABABC,k=4,p[j]=p[k]=p[4]=C 
//	2、若p[k ] ≠ p[j],如果此时p[ next[k] ] == p[j ],则next[ j + 1 ] =  next[k] + 1; //比如ABABC....ABABA,此时之前k=4(ABAB),因为p[4]!=p[j],next[k]=next[4]=2(ABAB);然后p[2]=p[j]=A,所以next[j+1]=next[k]+1=3(前缀串为ABA)
//否则继续递归前缀索引k = next[k],而后重复此过程,直到k=-1,那么next[j+1]=0。

void computePrefixFunction(char* p,int *next)  
{  
	int pLen = strlen(p);  
	next[0] = -1; //第一个字符的前缀串长度设为-1,第2个为0
	int k = -1;  
	int j = 0;  
	while (j < pLen - 1)  
	{  
		//p[k]表示前缀,p[j]表示后缀  
		//如果k=-1,则next[j]=0,即前j-1个字符没有前缀,或者如果k!= -1且p[j] == p[k],则前j-1个字符的next=k + 1(k先自增然后赋给next),然后求下一个字符的next
		if (k == -1 || p[j] == p[k])   
		{  
			++k;  
			++j;
			next[j] = k;  			 
		}  
		else   
		//如果k != -1,则把next[k]赋给k,继续找当前字符j之前的前缀串长度
		{  
			k = next[k];  
		}  
	}  
}  

int KmpSearch(char* s, char* p)  
{  
	int i = 0;  
	int j = 0;  
	int sLen = strlen(s);  
	int pLen = strlen(p);  
	int *next = (int *)malloc(sizeof(int) * pLen);//存储p中第i个字符前的字符串的前缀串长度(比如ABCABD,当前字符为D,下标为5,则p[5]=2,"AB"="AB")
	computePrefixFunction(p,next);
	while (i < sLen && j < pLen)  
	{  
		////这里是July原文:①如果j = -1,或者当前字符匹配成功(即S[i] == P[j]),都令i++,j++      
		////j=-1说明j = next[j],即当前字符j没有前缀串,故j要重头开始重新查找
		//if (j == -1 || s[i] == p[j])  
		//{  
			//i++;  
			//j++;  //如果j = -1 ,则此时j = 0 ,即j又从头开始判断
		//}  
		//else  
		//{  
			////②如果j != -1,且当前字符匹配失败(即S[i] != P[j]),则令 i 不变,j = next[j]      
			////next[j]即为j所对应的next值        
			//j = next[j];  //j调回到最长前缀的下一个字符,比如ABC....ABD,因为next[D]=2,则j调回到C,C再重新和i判断。比如ABC...MND,则next[D]=-1,则j=-1,下次判断后j回到0,重新判断
		//} 
		//如果当前字符相等,则i++,j++ (注:这里是我修改了一下)
		if(s[i] == p[j])
		{
			i++;
			j++;
		}
		else
		//否则,j退回到next[j]
		{
			j=next[j];
			//如果退回后的j为-1,说明之前比较的就是第一个字符,即next之前的j=0,因为只有next[0]=-1,则令j=0,从头判断,并且i++,比如s="ABC",p="DEF",'A'!='D'则重新判断'B'和'D'
			//注意如果退回后的j=0,则j也退到了p[0],但是这时i不能++,因为s[i]有可能=p[0],比如说s="...ABCABCD",P="ABCD",当判断到A!=D时,j退回到了0,这是还要继续判断s[i]是否='A‘
			if(j == -1 )//j=-1,说明上个j为第1个字符,即p[0],即[0]!=p[i],那么令j=0,i则向后挪一位
			{
				j=0;
				i++;				
			}
		}	
	}  
	free(next);
	if (j == pLen)  
		return i - j;  
	else  
		return -1;  
}  
int main()
{
	char *s="xyzabcbadmnnext";
	char *p="abcbad";	
	cout<<KmpSearch(s,p)<<endl;
	//cout<<ViolentMatch(s,p)<<endl;
	return 0;
}



你可能感兴趣的:(KMP,算法导论)