求子串的KMP算法

求子串的KMP算法

【题目】

  给定两个字符串str和match,长度分别为N和M,实现一个算法,如果字符串str中含有子串match,则返回match在str中的开始位置,不含有则返回-1;

【举例】

  str = “acbc” ,match = “bc”, 返回2。

  str = “acbc”, match = “bcc”, 返回-1。

【要求】

  如果match的长度大于str的长度(M>N),str必然不会含有match,可直接返回-1.但如果N>=M,要求算法复杂度为O(N)


【解答】

什么是KMP算法

  KMP算法是一种改进的字符串匹配算法,由D.E.Knuth与V.R.Pratt和J.H.Morris同时发现,因此人们称它为克努特–莫里斯–普拉特操作(简称KMP算法)。KMP算法的关键是利用匹配失败后的信息,尽量减少模式串与主串的匹配次数以达到快速匹配的目的。


字符串匹配的普通解法

  最普通的解法就是依次遍历str字符串的每一个字符,直到匹配match。

举个例子:str=”aaaaaaaaaaaaaaaaab”,match=”aaaab”。

1、刚开始匹配从str[0]开始,str[0]=match[0] … 直到str[4] != match[4],发现匹配失败,下一次从str[1]开始匹配

2、从str[1]重新开始匹配,str[1] = match[0] … 直到str[5] != match[4],又发现不匹配,下一次从str[2]开始匹配

3、从str[2]重新开始匹配,同样到了str[6] != match[4]发现匹配失败,如此下去,直到从str[13]开始匹配,str[13] = match[0],str[14] = match[1]  … str[17] = match[4],这时匹配成功,返回子串开始位置13。

这样的匹配发现时间复杂度特别高,每次都得重新挨个字符匹配,前面已经匹配过的信息没有利用好,从每个字符出发时,匹配代价都可能是O(M),一共N个字符,那么时间复杂度就是O(N x M)。


KMP算法可以快速匹配子串

1、先解释一下解题中用到的几个名词,分别是前缀子串、后缀子串、nextArr数组。

  前缀子串:是在match字符串中match[i]之前的,必须以match[0]开头而且不包含match[i-1]的子串

  后缀子串:是在match字符串中match[i]之前的,必须以match[i-1]结尾而且不包含match[0]的子串

  nextArr数组:nextArr[i]的含义是在match[i]之前的字符串match[0..i-1]中,后缀子串和前缀子串的最大匹配长度,这个长度就是nextArr[i]的值,nextArr数组是从匹配字符串match中得出的。

这里举例几个典型例子来形象的说明一下这3个名称的关系

  比如 match = “abc1abc1″,求一下nextArr[7],match[7]=’1’之前的子串是”abc1abc”,那么前缀子串和后缀子串的最大匹配是”abc”,也就是前缀子串是”abc”,后缀子串是”abc”时匹配度达到最大,那么nextArr[7] = 3,这里前缀子串包含(a,ab,abc,abc1,abc1a,abc1ab),后缀子串包含(c,bc,abc,1abc,c1abc,bc1abc),我们取前缀子串和后缀子串中匹配度最大的”abc”;

  再比如match = “aaaaaaab”,求一下nextArr[7],match[7]=’b’之前的子串是”aaaaaaa”,那么前缀子串和后缀子串最大匹配是”aaaaaa”,前缀子串和后缀子串都是”aaaaaa”,那么nextArr[7] = 6;

  至于如何得到nextArr数组,我先介绍完KMP算法之后再进行说明,这里先假设已经得到nextArr数组。

2、下面介绍如何加速str和match的匹配过程

  假设从str[i]字符位置出发,匹配到str[j]位置时发现与match[j-i]中的字符不一致,也就是说,str[i]与

match[0]一样,直到str[j-1]与match[j-i-1]的字符都是匹配的,但是到了str[i] != match[j-i],匹配失败

《求子串的KMP算法》

  因为现在有match字符串的nextArr数组,nextArr[j-i]的值表示match[0..j-i-1]这一段字符串前缀与后缀的最长匹配。假设前缀是下图a区域这一段,后缀是下图b区域这一段,再假设a区域的下一个字符为match[k],如下图:

《求子串的KMP算法》

  此时nextArr[j-i]的值是前缀和后缀的最大匹配长度,那么nextArr[j-i] = k;下一次的匹配不会是像普通解法那样退回到str[i+1]位置重新开始与match[0]匹配,而是直接滑动到str[j]与match[k]进行匹配检查,如下图:

《求子串的KMP算法》

  此处有个疑问,为什么从match[k]开始匹配呢,match[0..k-1]的字符为什么不进行匹配了呢?看下图所示,

图中str和match两个字符串匹配到A和B字符时发生匹配失败,那么c和b区域字符是相等的,而a和b是后缀字符和前缀字符的最长匹配字符串,也就是说a和b相等,同时,a和c也是相等的,a区域的下一个字符是C,当match滑动到A和C两个字符进行匹配时,其前面的c和a两个区域因为是相等的所以可以省略检查,这样我们对已经匹配过的字符进行了合理的利用,加快匹配效率。

  《求子串的KMP算法》

  然而,为什么中间存在一部分”不用检查”区域呢,因为在这个区域中,从任何一个字符出发都肯定匹配不出match。其实这个问题的答案我们可以从nextArr[j-1]中得出结论,nextArr[j-i]的含义是在match字符串中,从

match[0…j-i]前缀子串和后缀子串的最长匹配长度,此时在”不用检查”区域必然不存在一个字符,比b区域更大

的区域出现,这时b和c区域已经是在str和match的匹配过的字符串中重复度最大的值,也就是nextArr数组的值,所以,在向右滑动过的区域中必然会匹配不出,可以直接略过从而向右滑动。


  从整个匹配过程分析可以看到,str的匹配位置是不退回的,match一直在向右滑动。如果在str中某个位置完全匹配出match,整个过程停止。否则match滑动到str最右侧也最终停止,所以滑动的最大长度是N,那么时间复杂度就是O(N),匹配过程getIndexOf方法的代码如下:

public static int getIndexOf(String str, String match) {

		// 如果str的长度n小于match的长度m,直接返回-1
		if (str == null || match == null || str.length() < match.length()) {
			return -1;
		}

		char[] ss = str.toCharArray();
		char[] ms = match.toCharArray();
		int si = 0;
		int mi = 0;
		int[] nextArr = getNextArr(ms);// 获得nextArr数组
		while (si < ss.length && mi < ms.length) {
			// 如果str和match中字符挨个相等,则同时向后移动
			if (ss[si] == ms[mi]) {
				si++;
				mi++;
			}
			// 如果str和match中没有前缀相等的字符,也就是没有可以省略匹配的字符,则从match[0]开始重新挨个匹配
			else if (nextArr[mi] == -1) {
				si++;
			}
			// 如果str和match中遇到一个不相等的字符,且nextArr[mi]!=-1,说明有重复的字符,下次匹配就从
			// match[k]开始,nextArr[mi]=k;
			else {
				mi = nextArr[mi];
			}
		}

		return mi == ms.length ? si - mi : -1;
	}

   
接下来我们就来说一下如何快速得到match字符串的nextArr数组,并且证明得到nextArr数组的时间复杂度为O(M)。对match[0]来说,在它之前没有字符,所以nextArr[0]=-1;对match[1]来说,在它之前只有一个字符,

但nextArr数组的定义要求任何子串的后缀不能包括第一个字符(match[0]),所以match[1]之前的字符串只有长度为0的后缀子串,所以nextArr[1]=0。

之后对match[i](i>1)的求解,过程如下:

1、因为是从左到右依次求解nextArr,所以在求nextArr[i]时,nextArr[0..i-1]的值已经求出。假设match[i]字符为下图中的A字符,match[i-1]为B字符,通过nextArr[i-1]的值可以知道B字符前的子串最长前缀和后缀的匹配区域,如图的m区域是前缀子串,n区域是后缀子串,字符C是m区域之后的一个字符,然后看字符C和字符B是否相等。

《求子串的KMP算法》

2、如果字符C与字符B相等,那么A字符之前的字符串最长前缀(m区域+C字符)与后缀(n区域+B字符)匹配区域就是

nextArr[i] = nextArr[i-1] + 1;

3、如果字符C与字符B不相等,就看字符C之前的前缀和后缀匹配情况,假设字符C是第cn个字符(match[cn]),那么

nextArr[cn]就是其最长前缀和后缀的匹配长度,如下图:

《求子串的KMP算法》

  在上图中,n1区域和n2区域分别是字符C之前的字符串最长匹配好后缀与前缀区域,这是通过nextArr[cn]的值确定的,m1区域是m区域最右边的长度和n2区域一样,因为m和n区域相等,所以m1和n2区域相等,那么m1和n1区域也相等,字符D是n1区域后的一个字符,接下来比较字符D和字符B是否相等。

(1)、如果相等,那么字符A的前缀子串就是 n1区域+D字符,后缀子串就是 m1区域+B字符,则nextArr[i] = nextArr[cn] + 1;

(2)、如果不相等,就跳到字符D,寻找D的前缀子串和后缀子串,与字符C一样,如此循环,这样每一步都有一个字符与字符B进行比较,如果有相等的情况出现,那么nextArr[i]的值就可以确定;

4、如果跳到了最左边的位置(match[0]),此时match[0] = -1;说明字符A之前的字符串不存在重复的字符,则令

nextArr[i] = 0。这样下去就可以得出nextArr数组值,求解代码getNextArr代码如下:

public static int[] getNextArr(char[] ms) {

		// 如果ms长度就一个字符,直接返回-1
		if (ms.length == 1) {
			return new int[] { -1 };
		}
		int[] nextArr = new int[ms.length];
		nextArr[0] = -1;// match[0]之前没有字符
		nextArr[1] = 0;// match[1]之前只有一个字符,后缀子串长度是0
		int pos = 2; // 从match[2]开始
		int cn = 0;
		while (pos < nextArr.length) {
			// 如果match[cn]和match[pos-1]相等,那么nextArr[i] = nextArr[i-1] + 1
			//  nextArr[i-1]=cn,那么nextArr[i]=cn +1;
			if (ms[pos - 1] == ms[cn]) {
				nextArr[pos++] = ++cn;
			} 
			//cn>0,还没有循环到最左侧,继续比较nextArr[cn];
			else if (cn > 0) {
				cn = nextArr[cn];
			}
			//直到cn==1,没有重复的字符,那么nextArr[pos] = 0;
			else {
				nextArr[pos++] = 0;
			}
		}

		return nextArr;
	}






    原文作者:KMP算法
    原文地址: https://blog.csdn.net/u010456903/article/details/48899551
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞