KMP算法理解(更新)

KMP算法是一个非常经典的字符串匹配的算法,它讲的是,我们给定两个字符串str1与str2,长度分别问N和M,实现一个算法,如果字符串str1,包含str2,则返回str2在str1中的开始位置,不包含则返回-1。题意很容易理解,如下图:当str1与str2为abcdabce与abce的时候,二者匹配,返回abce在str1中开始位置的下标3,当str1与str2为abcdabce与abcf的时候,二者不存在包含关系,返回-1。大多数人看到这个题目,第一个想到的方法肯定是暴力比对,将str2从str1的第一个字符位置开始比对,如果能够匹配上则返回,不能匹配就从第二个,第三个…直到匹配上或者匹配失败,不可否认,采用这种方法确实能够完成需求,但是所耗费的时间复杂度是O(M*N),那么有没有什么能够降低时间复杂度的办法呢,我们的KMP算法就闪亮登场了,在开始介绍之前,我们先了解一个概念,字符串str的nextArr数组,这个数组有什么特点:1.这个数组的长度与str字符串的长度一样2.nextArr[i]的含义就是str[i]之前的字符串str[0…i-1]中必须以str[i-1]结尾的后缀子串,(不能包含str[0])与必须以str[0]开头的前缀子串(不能包含str[i-1])的最大匹配长度。下面举个简单的例子,大家就明白了,假设我们有一个字符串str为abcdabcd,那么它的nextArr数组是怎么得出来的呢,先把结果列出来,结果就是:[-1,0,0,0,0,1,2,3]。下面就详细介绍一下这个结果是怎么出来的: 1. 首先当i=0的时候,str[0]之前没有任何字符串了,默认此时值为-1 2. 接下来i=2的时候,str[1]之前只有一个字符串a,此时默认为0 3. 接下来i=2,str[2]之前的字符串为ab,此时a与b不等,nextArr[2]=0 4. 接下来i=3,str[3]之前的字符串为abc,nextArr[3]=0 5. 同理,nextArr[4]=0 6. 一直到i=5的时候,str[5]之前是abcda,此时有一个字符a,是前缀子串与后缀子串的匹配字符,nextArr[5]=1 7. 当i=6的时候,str[5]之前是abcdab,此时有一个字符串ab,是前缀子串与后缀子串的最长匹配,nextArr[6]=2 8. 同理nextArr[7]=3下面看当我们得到这个nextArr数组之后,它是如何优化时间复杂度的。回到我们最初的问题上来,我们要解决的问题是判断字符串str1是否包含str2如上图所示,a串与b串是字符串str2[0…j-1]的前缀子串与后缀子串相匹配的最长字符串,即a与b相等,假设str1与str2在叉号左边部分完全匹配上了,但是到str2[j]的位置匹配失败,即str2[j]不等于str1[i],此时,注意,我们的做法不再是将str2只向右滑动一个单位,而是向右滑动j-nextarr[j] (已匹配长度-前缀与后缀最大公共长度)个单位,然后再继续以上过程,开始匹配,这样就完成了时间复杂度的优化工作,这一步也是KMP算法的核心步骤。现在假设我们有一个方法可以获取一个字符串的nextArr数组,假设这个方法名为getNextArr,下面附上KMP算法的代码: pubic int getIndex (String str1, String str2){
//如果两个字符串都为空,或者匹配串的长度大于被匹配串的长度,直接返回-1
if (str1 == null || str2 == null || str2.length > str1.length) {
return -1;
}
char[] ch1 = str1.toCharArray();
char[] ch2 = str2.toCharArray();
//i,j分别表示在str1与str2的指针,当j走到str2的最后一个字符的时候,说明匹配成功。
int i = 0;
int j = 0;
int[] nextArr = getNextArray(ch2);
while (i < ch1.length && j < ch2.length) {
//如果匹配上就进行下个位置的对比
if (ch1[i] == ch2[j]) {
i++;
j++;
}
//如果nextArr[j]==-1,则说明匹配串index为0,只有此处默认值为-1;
else if (nextArr[j] == -1) {
i++;
}
//否则,匹配串向右滑动,这里的j = nextArr[j]可以理解为向右滑动
else {
j = nextArr[j];
}
}
return j == ch2.length ? i - j : -1;

	}接下来就介绍一下如何获取nextArr数组前面就介绍过按照规定字符串第一个字符对应的数组值为-1,第二个为0,即nextArr[0] = -1;nextArr[1] = 0;对于后面的求解过程,下面详细介绍:因为是从左到右依次求解,所以当求解nextArr[i]的时候,nextArr[i-1]已经求解出来,通过它的值可以知道B字符前字符串的最长前缀与最长后缀的匹配区域,a区域与b区域,字符C与字符B分别是紧贴着这两个区域后面的字符,由此可知,如果C字符与B字符相同,那么nextArr[i]=nextArr[i-1]+1。如果字符C与字符B不等,那么就看字符C之前的前缀与后缀的匹配情况了,假设字符C是第cn个字符,那么nextArr[cn]就是其最长前缀与后缀匹配的长度,如下图所示,那么,n与m两个就是最长前缀与后缀区域,m'是b区域的最右区域且长度与m区域长度一致,那么m与m'一定是相等的,字符D是n区域后面一个元素,如果D字符与B字符相等,那么nextArr[i]=nextArr[cn]+1。如果不等那么继续往前跳到字符D,之后的过程与跳到C一致,每跳一次都会出现一个字符与B比较,如果相等,nextArr[i]就可以确定。如果跳到最左的位置,此时nextArr[0]=-1,此时说明字符A之前的字符串不存在前缀后缀匹配,令nextArr[i]=0;具体代码如下:public int[] getNextArray (String s){
		char[] ch = s.toCharArray();
		if (ch.length == 1) {
			return new int[]{-1};
		}
		int[] nextArr = new int[ch.length];
		nextArr[0] = -1;
		nextArr[1] = 0;
		int pos = 2;
		int cn = 0;
		while (pos < next.length) {
		//如果字符B等于字符C,加一
			if (ch[pos - 1] == ch[cn]) {
				next[pos++] = ++cn;
			} else if (cn > 0) {
				cn = next[cn];
			} else {
				next[pos++] = 0;
			}
		}

		return next;

	}

猜你喜欢

转载自blog.csdn.net/weixin_44313315/article/details/106875841