串匹配算法KMP详解

串匹配问题:给定两个字符串S和T,在主串中查找模式子串T的过程称为串匹配,结果得到的是模式子串T在主串S中的位置。
在讲解KMP算法之前,先简单介绍最容易想到的串匹配算法:蛮力法或称之为朴素的串(模式)匹配算法
蛮力法:先从主串的第一个字符开始和模式T的第一个字符进行比较,若相等,继续比较两者后续的字符;若不相等,则从主串的第二个字符重新开始和T的第一个字符比较,重复以上操作,如果T中字符全部比较完毕,则说明匹配成功,返回成功时的位置;若S中的字符已经比较完毕且T中字符还未比较完毕,则说明匹配失败。该算法思想和代码如下所示:
/***************************** 蛮力法 ********************************/
//BF算法:朴素的模式(串)匹配算法
/*  输入:主串S,待匹配的模式串T
输出:T在S中所处的位置
1. 初始化主串比较的开始位置index=0(或者index=pos;此时表明从S的pos位置往后开始匹配T)
2. 初始化主串S和匹配串T比较的起始位置i=0和j=0;
3. 重复以下操作,直到S或T的所有字符都已比较完毕:
3.1 如果S[i]==T[j],则同时移动S和T到下一个字符继续比较;
3.2 否则,下一趟匹配的主串的开始位置index++,i和j分别回溯到i=index和j=0;
4. 如果T中的所有字符都已比较完毕,则返回匹配的开始位置index,否则返回0.      */
//以下为算法的C语言实现代码

int BF(char S[], char T[])
{
	int i, j, index;
	i = j = index = 0;
	while(S[i] != '\0' && T[j] != '\0')
	{
		if(S[i] == T[j])
		{
			i++;
			j++;
		}
		else                 //i和j分别回溯
		{
			index++;
			i = index;
			j = 0;
		}
	}
	if(T[j] == '\0')
		return index+1;      //返回本趟匹配成功的起始位置
	else
		return 0;
}
//从主串S的pos位置开始比较

int BF(char S[], char T[], int pos)
{
	int i = pos;
	int j = 0;
	while(S[i+j] != '\0' && T[j] != '\0')
	{
		if(S[i+j] == T[j])
		{
			j++;
		}
		else
		{
			i++;
			j = 0;
		}
	}
	if(T[j] == '\0')
		return i+1;
	else
		return 0;
}

但是这种方法最大的问题是回溯引起的低效率。所谓回溯是指在某趟匹配失败后,主串S要回到本趟匹配开始字符的下一个字符,而模式T需要从头开始比较。其实这种回溯多数情况下是不必要的或者说是部分不必要的。
比如说S=“abcabcac”,T=“abcac”,当我们第一趟比较中,最后一个字符不匹配,因此回溯后,第二趟让T的第一个字符和S的第二个字符开始比较,其实因为S[1]=T[1],T[0]不等于T[1],所以T[0]不等于S[1],因此这趟比较是多余的。同理,第三趟也是多余的。进一步我们发现第四趟第一对字符的比较也是多余的,因为T[0]=T[3],S[3]=T[3],因此S[3]=T[0]。所以第四趟可以从第二对字符S[4]和T[1]开始比较,即S不回溯,T回溯到第二个字符,在进行下一步的比较。
那么,这种做法能保证不会漏掉某个答案吗?
首先,如果模式串是连续多个相同字符的话,比如T=“aaaaa”,S=“aaaabaaaaa”,此时不需要回溯,因为此时如果前四个匹配正确,而第五个匹配失败的话,我们只需要从第六个开始第二趟比较,此时不需要回溯....
其次,你可能担心以下情况,即如果前四个匹配,第五个不匹配,正如前面的例子,难道直接拿主串第七个字符和模式串比较?如果从主串的第二个字符开始就匹配成功了呢?这样是不是就漏掉了某些答案啊或者说就是错的?
这就是KMP算法最迷人的地方。下面我们来证明这样做既不会漏掉答案,而且会因此更加高效:
如果是第六个匹配失败的话,那么算法会选择从T的某个位置index开始与主串当前匹配失败的位置的下一个位置开始比较,这个index具体求法后面再说。当第六个匹配失败时,前面四个匹配正确,那么应该如何选择index呢?此时我们有:


这五种情况任何一种情况都是拿模式串的每一位开始与主串进行比较,这个位置就是index,所以主串不需要回溯,我们只需要知道index的值即可。
这个值跟主串无关,只跟子串自身有关。而且子串的每一个位置都有对应的一个index值,我们把他们放入数组next中。
我们希望在某趟S[i]和T[j]匹配失败后,下标i不回溯。j回溯到某个位置k(就是前面说的index,下文统一用k表示),从T[k]开始和S[i]进行比较,那么如何确定k呢?
在部分匹配成功时,在某趟S[i]和T[j]匹配失败后,下一趟比较从T[k]开始和S[i]进行比较,则我们可以得到关系:
T[0]~T[k-1]==S[i-k]~S[i-1];并且在j回溯至k之前,我们可知:T[j-k]~T[j-1]==S[i-k]~S[i-1].由此我们可知关系式:T[0]~T[k-1]==T[j-k]~T[j-1]。
上式说明模式T的每一个字符T[j]都对应着一个k值,而且这个k值仅仅依赖于模式T本身,与主串无关。T[0]~T[k-1]是T[0]~T[j-1]的真前缀,T[j-k]~T[j-1]是T[0]~T[j-1]的真后缀,k是T[0]~T[j-1]的真前缀和真后缀相等的最大长度。(字符串的前缀是指字符串的任意首部。字符串的后缀是指字符串的任意尾部。)
我们用next[j]来表示T[j]对应的k值,其定义如下:
j=0, next[j]=-1;
next[j]=max{k|1<=k<j且满足T[0]~T[k-1]==T[j-k]~T[j-1]};
否则,next[j]=0;
下面给出next[]数组的求法:

void GetNext(char T[], int next[])
{
	int i, j, k;
	next[0] = -1;
	for(j = 1; T[j] != '\0'; j++)    //依次求next[j]
	{
		for(k = j-1; k >= 1; k--)      //相等子串的最大长度为j-1
		{
			for(i = 0; i < k; i++)       //依次比较T[0]~T[k-1]与T[j-k]~T[j-1]
			{
				if(T[j-k+i] != T[i])
					break;
			}
			if(i == k)
			{
				next[j] = k;
				break;
			}
		}
		if(k < 1)
			next[j] = 0;             //其他情况,无相等字串
	} 
}

此为蛮力法求next[]数组,蛮力法求模式T的next值,时间复杂度为O(m^3)。
假设我们已经计算出next[0],next[1]...next[j],那么如何求出next[j+1]呢?
设next[j]=k,则说明T[0]...T[k-1]==T[j-k]...T[j-1],即T[0]...T[k-1]是
T[0]...T[j-1]的真前缀,T[j-k]...T[j-1]是T[0]...T[j-1]的真后缀。
那么如何计算next[j+1]呢?此时有两种情况:
(1)T[j]=T[k],这说明T[0]...T[k-1]T[k]==T[j-k]...T[j-1]T[j],有定义知:next[j+1]=k+1;
(2)T[j]!=T[k],此时我们要找出T[0]...T[j]的真前缀和真后缀相等的最长子串。我们可以先找出T[0]...T[j-1]的真前缀和真后缀相等的第二长子串,假设位置为m,则可知T[0]...T[m-1]==T[j-m]...T[j-1],此时比较T[m]和T[j],同样两种情况,若相等,则T[0]...T[m-1]T[m]==T[j-m]...T[j-1]T[j],有定义知:next[j+1]=m+1;如不相等,则继续求T[0]...T[j-1]的真前缀和真后缀相等的第三长子串,重复以上过程,直到某个位置n,(1)有T[n]=T[j],得到next[j+1]=n+1,(2)或者next[n]=-1,说明T[0]...T[j]无相等真前缀和真后缀,令next[j+1]=0.
那么这个位置m或者n怎么求呢?看一下推导过程:
因为  T[0]...T[k-1]==T[j-k]...T[j-1]
所以  T[k-next[k]]...T[k-1]==T[j-next[k]]...T[j-1]
因为  T[0]...T[next[k]-1]==T[k-next[k]]...T[k-1]
所以  T[0]...T[next[k]-1]==T[j-next[k]]...T[j-1]
所以说其实m=next[next[j]]=next[k],因为T[0]...T[next[k]-1](即T[0]...T[m-1])为
T[0]...T[k-1]最长的真前缀和真后缀相等的子串,同时也是T[0]...T[j-1]的真前缀和真后缀相等的子串中第二长的,这就是m。同理,第三长、第四长...的位置均可如此求出。

//改进求模式T的next的算法,时间复杂度为O(m)

void GetNext(char T[], int next[])
{
	int i, j, k;
	i = j = 0;
	k = -1;
	next[0] = -1;
	while(T[j] != '\0')       
	{
		if(-1 == k)                   //起始时无相同子串
		{
			next[++j] = 0;
			k = 0;
		}
		else if(T[j] == T[k])        //当前值与T[k]相等时,确定next[j+1]的值
		{
			next[++j] = k+1; 
			++k;
		}
		else                        //不相等时,继续寻找T[0]~T[k]的相等子串作为T[0]~T[j]的相等子串(真前缀与真后缀相等的最大子串)
		{
			k = next[k]; 
		}
	}
}

算法只需将T扫面一遍,若模式长度为m,则时间复杂度为O(m)。

在求得next[]数组之后,KMP算法只需将主串扫描一遍,设主串长度为n,则KMP算法的时间复杂度为O(n)。

<pre code_snippet_id="139487" snippet_file_name="blog_20140102_1_1120069" name="code" class="html"><pre code_snippet_id="139487" snippet_file_name="blog_20140102_1_1120069" name="code" class="html"><pre code_snippet_id="139487" snippet_file_name="blog_20140102_1_1120069" name="code" class="html"><pre code_snippet_id="139487" snippet_file_name="blog_20140102_1_1120069" name="code" class="html"><pre code_snippet_id="139487" snippet_file_name="blog_20140102_1_1120069" name="code" class="html"><pre code_snippet_id="139487" snippet_file_name="blog_20140102_1_1120069" name="code" class="html" style="font-size: 18px;"><pre code_snippet_id="139487" snippet_file_name="blog_20140102_1_1120069" name="code" class="html" style="font-size: 18px;"><pre code_snippet_id="139487" snippet_file_name="blog_20140102_1_1120069" name="code" class="html" style="font-size: 18px;"><pre code_snippet_id="139487" snippet_file_name="blog_20140102_1_1120069" name="code" class="html"><pre code_snippet_id="139487" snippet_file_name="blog_20140102_1_1120069" name="code" class="html" style="font-size: 18px;"><pre code_snippet_id="139487" snippet_file_name="blog_20140102_1_1120069" name="code" class="html"><p></p><pre code_snippet_id="139487" snippet_file_name="blog_20140102_1_1120069">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

                
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值