字符串模式匹配算法

看了一整天数学有点头疼,似乎也没什么可以写的。看了下ACM寒假训练的安排最后一周貌似有学长要讲KMP字符串模式匹配算法,那今天就整理一下这个吧。

什么是字符串模式匹配

假设指定两个串T和P
T = “t0t1t2…t(n-1)” , P = “p0p1…p(m-1)”
其中,0 < m <= n.如要在字符串T中查找是否有与字符串P相同的子串,则称字符串T为目标串或主串,称字符串P为模式串或子串,在T中查找与P相同的子串第一次出现的位置的过程称为字符串模式匹配。
顺便提一下,字符串模式匹配的应用范围越来越广,如在文本编辑中搜说某个子文本,又如分子生物学中人们用模式串匹配算法从DNA序列中提取信息。
所以仅仅会使用c语言中的strstr()库函数是远远不够的,这里整理模式匹配操作的几种实现思想。

简单字符串模式匹配算法与首尾字符串模式匹配算法

简单匹配算法又称为BF算法,这种最简单的实现方式就是用字符串P的字符依次与字符串T中的字符进行比较。首先将子串P从第0个字符起与主串T的第pos个字符起一次比较对应字符,如果全部对应相等则表明已找到匹配;否则,将子串P从第0个字符起与主串第pos+1个字符起依次比较对应字符,过程与前面相似;如此进行直到匹配成功或T中没有足够剩余字符与P中各字符比较(匹配失败)为止。
不失一般性的,pos = 0.
代码如下:

int SimInd(const string &T,const string &P,int pos = 0)
{
	int i = pos , j = 0;
	while(i < T.length() && j < P.length())
	{
		if(T[i] == P[j])
		{
			i++;
			j++;
		}
		else
		{
			i = i - j + 1;//返回到pos+1
			j = 0;
		}
	}
	if(j >= P.length())
		return i-j;//匹配成功
	else return -1;//匹配失败
}

在《数据结构教程》(李春葆著)中没有提及所谓首尾匹配法,但我认为这种算法总归有他的用处,万一什么时候用的到呢。首尾匹配在处理模式串尾的不匹配时有优点,但是当模式串的不匹配在中间位置时,这种方法的效率反而会降低。
该算法具体实现如下:

int FrBeInd(const string &T,const string &P,int pos = 0)
{
	int i = pos;
	while(i < T.length() - P.length() + 1)
	{
		int fi = 0,bi = P.length() - 1;
		while(fi < bi)
		{
			if(T[i+fi] != P[fi] || T[i+bi] != P[bi]) break;
			else 
			{
				fi++;
				bi--;
			}
			if(fi < bi)
				return i;
			else	i++;
		}
	}
	return -1;
}

上面两种算法在一次匹配失败时,下次匹配开始都是将P后移一个位置,再从头开始与主串中的对应字符进行比较。造成算法效率低的主要原因是执行过程中的回溯。下面讨论避免这些回溯的KMP算法。

KMP算法

(上次看KMP算法的时候头疼了好一阵,下面的全是重点!!)
例1. T = { a , b , a , a , a , b}
P = { a , b , a , b}
第一次匹配时有t0 = p0 , t1 = p1 , t2 = p2 , t3 != p3,在模式串P中有,p0 != p1,所以有 t0 != p1,所以若只将P右移一位的话,匹配一定不成功。而p0 = p2,所以t2 = p0,在第三次匹配中P右移一位,t2 一定等于 p0,所以P应该直接右移2位并且跳过t2与p0的比较,直接从t3 与 p1开始进行匹配,这个过程就避免了回溯。
通过上面的例子大家应该大致了解了KMP算法的过程,下面讨论一般情况。
BF算法执行到i+1(位置是i)次匹配时,如果比较到P中的第j个字符时不匹配,即
“ti ti+1 ti+2…ti+j-1” = “p0p1…pj-1” (1)
如果按照BF算法的思路下一次匹配应该从T中的i+1位置起用ti+1与P中的p0开始比较。匹配成功时,有
“ti+1 ti+2 … ti+j … ti+m” = “p0p1 … pm-1” (2)
如果P有(3)式的特征
“p0 p1 … pj-2” != “p1 p2 … pj-1” (3)
此时由(1)式可知
“ti+1 ti+2 …ti+j-1” = “p1 p2…pj-1” (4)
由(3)(4)式可推知
“ti+1 ti+2 … ti+j-1” != “p0 p1 … pj-2” (5)
所以可以得到这样的关系
“ti+1 ti+2 … ti+m” != “p0 p1 … pm-1” (6)
所以第i+2次的匹配就可以跳过了,类似的,可以推知当P中有
“p0 p1 … pj-3” != “p2 p3 … pj-1” (7)
时 “ti+2 ti+3 … ti+m+1” != “p0 p1 … pm-1”,这一次匹配依然可以跳过。
依次类推,直到对于某一个值k,使得
“p0 p1 … pk” != “pj-k-1 pj-k … pj-1” (8)
并且
“p0 p1 … pk-1” = “pj-k pj-k+1 … pj-1” (9)
这个时候就可以得到下面的式子
“p0 p1 … pk-1” = “pj-k pj-k+1 … pj-1” = “ti+j-k ti+j-k+1 … ti+j-1” (10)
这样,在第i+1次匹配中,如果T中的第i+j个字符与P中的第j个字符不匹配,只需要将P向右移动j-k位,使pk与ti+j对齐并开始比较。原因如上分析。
KMP算法的关键是匹配失败时T的扫描指针不回溯,P的扫描指针退回到pk的位置,所以需要确定k的值。根据前面的分析我们知道,k的值只与j和P有关,而和T无关。这里设next[j] = k,表示 P中第j个字符不匹配时,应当由P中第k个字符与刚才T中的位置继续进行匹配。
对P的next[j]进行定义:
j = 0时,next[j] = -1;
集合非空时,next[j] = max{k|0<k<j且"p0p1…pk-1"=“pj-k pj-k+1 … pj-1”};
其他情况,next[j] = 0;
给出已知P的next[j]时的算法:

int KMP(const string &T,const string &P,int pos,int next[])
{
	int i = pos,j = 0;
	while(i < T.length() && j < P.length())
	{
		if(j == -1)
		{
			i++;
			j = 0;
		}
		else if(P[j] == T[i])
		{
			i++;
			j++;
		}
		else
		{
			j = next[j];
		}
		if(j < P.length()) 
			return -1;
		else
			return i-j;
	}
}

这样,问题就转换为求P的next[j]了。要求next[j],就是要在"p0 p1…pj-1"中找出最长的相等的两个子串"p0 p1 … pk-1" 和 “pj-k pj-k+1 … pj-1”,明显求next的过程是一个递推的过程。
由前面的定义可以知道next[0] = -1;
假设已知next[j] = k,那么就有
“p0 p1 …pk-1” = “pj-k pj-k+1 … pj-1”
可以给出next[j+1]的定义:
集合非空时,next[j+1] = {max{k+1|0<k+1<j+1且"p0 p1 …pk" = “pj-k pj-k+1 … pj”}};
其他情况时,next[j+1] = 0;
如果pk = pj,可知,next[j+1] = k+1=next[j] +1.
如果pk!=pj,可得"p0 p1 …pk" != “pj-k pj-k+1 … pj”
这时就可以把求next[j+1]值得问题看成一个目标串和模式串都是P的模式匹配问题。即,在"p0 p1 …pk-1"中寻找一个最大的s,使得
“p0 p1 … ps-1” = "pk-s pk-s+1 … pk-1"成立。
1.存在这个s时,就有next[k] = s。有
“p0 p1 … ps-1” = “pk-s pk-s+1 … pk-1” = “pj-s pj-s+1 … pj-1”
如果ps = pj 就可以得到 next[j+1] = s + 1 = next[k] + 1 = next[next[j]] +1
如果ps != pj,我们就要在p0…ps-1中继续寻找更小的next[s],方法同上。
依次递推。
2.找不到s,此时next[j+1] = 0.

由此,得到next[j+1]的递推公式
能找到最小的正整数m,使得p(next(m)[j]) = pj, next(m)[j+1] = next(m)[j];
当找不到m或j=0时,next[j+1] = 0.
其中next(1)[j] = next[j],next(m)[j] = next[next(m-1)[j]].
最后给出计算P的next[j]的算法:

void Next(const string &P,int next[])
{
	next[0] = -1;
	int j = 0,k = -1;
	while(j < P.length() - 1)
	{
		if(k == -1)//此时j一定等于0
		{
			next[j+1] = 0;
			//next[1] = 0 ,所以
			j = 1;
			k = 0;
		}
		else if(P[k] == P[j])
		{
			next[j+1] = k+1;
			j++;
			k++;
		}
		else
			k = next[k];
	}
}

到这里KMP算法就算完结了,文中有不妥之处,希望各位大佬指出。

参考书目:
《数据结构》
《数据结构教程》
《数据结构与算法》

猜你喜欢

转载自blog.csdn.net/qm230825/article/details/86428074
今日推荐