KMP字符串模式匹配

字符串相关的处理在程序设计中占有很大的一部分篇幅,对于模式匹配的相关应用场景也多如牛毛,那么什么样的匹配算法才可以提高程序的执行效率呢

所谓的字符串模式匹配时例如这样: 有两个字符串T和S,字符串T称之为正文, 字符串S称之为模式, 要求找出模式S在正文T中首次出现的位置,这个查找的过程就是匹配的过程, 一旦查找成功了我们就说发生了一次模式匹配并且匹配成功,我们可以利用这个方式找到所有的匹配。


在讲解KMP之前,我们先讲解常规的朴素模式匹配算法,我称之为暴力匹配算法。

假设我们有两个字符串 A = “abcdefgabcdefx”, B= “abcdefx”;现在我们需要匹配B是否在A中出现,或者说B在A中第一次出现的位置,利用常规的算法我们会这样:

     

int Match(string A, string B)
{
	int i = 0, j = 0;
	while(i < A.length() && j < B.length())
	{
		if (A[i+j] == B[j])
		{
			j++;
		}
		else
		{
			i++;
			j = 0;
		}
	}

	if (j >= B.length()) 
		return i;
	else 
		return -1;
}

朴素的模式匹配在每次匹配失败后,都需要回溯i的位置到i+1的位置,j回朔到其实位置重新进行匹配,这样将带来的一个问题则是,当模式串各个位置都不相同时,如上的B串,B= “abcdefx”, 当j=6 即x是发生不匹配,此时j=6之前的模式匹配都已经成功,此时再回朔i=i+1, j = 0; 那么因为B中每个元素都不相同,所以i=i+1到i+j 的匹配都全部不成功,所以做了很多无用功。为了解决这一一个问题便有了KMP算法。


KMP算法在不匹配发生时,不会选择回朔i,从始至终I都不需要回朔, 而是根据子串求出的一个next数组对j进行右移,使得字符串匹配得以加速,KMP的匹配算法如下:


int KMP(string sstr, string pstr, int next[])
{
	int slen = sstr.length();
	int plen = pstr.length();
	int i = 0, j = 0;
	
	while(i < slen && j < plen)
	{
		if (j == -1 || sstr[i] == pstr[j])
		{
			i++;
			j++;
		}
		else
		{
			j = next[j];
		}
	}

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


从代码可以看出, 和朴素的模式匹配唯一不同的地方在于j=next[j], 而i的值始终都没有进行回溯操作, 那么匹配失败的时候我们可以看出KMP算法中,如果当前字符匹配成功,即sstr[i]==pstr[j],令i++j++,继续匹配下一个字符;如果匹配失败,即sstr[i] != pstr[j],需要保持i不变,并且让j = next[j],这里next[j] <=j -1,即模式串pstr相对于原始串sstr向右移动了至少1(移动的实际位数j - next[j]  >=1);  那么KMP中最重要的部分都来自于next数组, 那么next数组是如何求得的呢?


void get_next(int next[], string str)
{
	int i = 0, k = -1;
	int str_len = str.length()-1;

	next[0] = -1;

	while(i < str_len)
	{
		if (k == -1 || str[k] == str[i])
		{
			k++;
			i++;
			next[i] = k;
			continue;
		}
		k =  next[k];
	}
}

       那么next数组代表的是什么呢,其实就是模式串每一个位置的前后缀的最大公共长度。这个最大公共长度在算法导论里面被记为next数组。在这里要注意一点,next数组表示的是长度,下标从1开始;但是在遍历原字符串时,下标还是从0开始。假设我们现在已经求得next[1]、next[2]、……next[i],分别表示长度为1到i的字符串的前缀和后缀最大公共长度,现在要求next[i+1]。如果位置i和位置next[i]处的两个字符相同(下标从零开始),则next[i+1]等于next[i]加1。如果两个位置的字符不相同,我们可以将长度为next[i]的字符串继续分割,获得其最大公共长度next[next[i]],然后再和位置i的字符比较。这是因为长度为next[i]前缀和后缀都可以分割成上部的构造,如果位置next[next[i]]和位置i的字符相同,则next[i+1]就等于next[next[i]]加1。如果不相等,就可以继续分割长度为next[next[i]]的字符串,直到字符串长度为0为止。

求得了next数组,我们就可以用正常的使用kmp算法了, 它将时间复杂度从kmp算法的复杂度是O(n*m) 提高到了O(n+m)


对KMP算法的改进:

KMP算法虽然高效,但是人们后来发现KMP算法还是有缺陷存在, 比如当aaaabcde和aaaaax匹配时,next数组为{-1, 0, 1, 2, 3, 4}  当在第5个a处匹配失败时, 此时j = 4 变成 next[j] = 3, 也就是说模式串向右移动了4-3 = 1 位, 此时j= 3处还是a和主串i=4处的b仍旧不相等,匹配仍然失败,导致最终无效的匹配同样做了多次,降低效率。 我们观察发现第1位的a和第二三四五位的a都是一样的,那么我们很自然的可以用第一个a的next值去取代第二三四位的next值,溯本回原这样可以减少无效匹配的次数。改进后的next如下:

void get_nextval(int nextval[], string str)
{
	int i = 0;
	int k = -1;
	nextval[0] = -1;
	int str_len = str.length()-1;

	while(i < str_len)
	{
		if (k == -1 || str[k] == str[i])
		{
			i++;
			k++;
			if (str[k] != str[i])
				nextval[i] = k;
			else
				nextval[i] = nextval[k];
		}
		else
		{
			k = nextval[k];
		}
	}
}
改进过的KMP算法在计算出next值的同事, 如果i位的字符与他对应的next指向的字符相同, 则将i的next值指向他们共同的父值next[next[i]], 如果不相同,则i的next即为next[i]自己的值。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章