KMP算法理解及代码实现(通俗易懂版)

1 KMP算法

KMP算法使用来解决,一个字符串是否为另一个字符串的子的问题,
KMP算法之所以高效是  因为匹配失败时,总是能够让模式串回退到某个位置,使文本不用回退。
而其重点难点也就是在于模式串到底要回退多少才合适。

2 关于前缀 后缀

对于字符串 abcd  前缀包括:[a]  [ a b]  [a b c] ,后缀包括:[d]   [c d]  [ b c d],前后缀不包括字符串本身。

3 关于为什么要求模式串前缀后缀最长公共元素长度

例如给定文本串S "BBCABCDABABCDABCDABDE",和模式串P "ABCDABD",在以下的某一个时刻,模式串P字符D与主串S字符A不匹配了,如下:

...

...

...

A

B

C

D

A

B

A

B

...

...

...

 

 

 

A

B

C

D

A

B

D

 

 

 

 

 

KMP算法会保持指向主串的指针不动,即保持在A处,根据已经匹配的字符串 ABCDAB,移动模式串到合适的位置,再次进行扫描匹配,如下

...

...

...

A

B

C

D

A

B

A

B

...

...

...

 

 

 

 

 

 

 

A

B

C

D

A

B

D

再次进行扫描时,主串指针向A,而模式串指向C,在移动之前,主串与模式串已经匹配的字符为ABCDAB,而ABCDAB的前缀后缀最长公共元素AB

...

...

...

A

B

C

D

A

B

A

B

...

...

...

 

 

 

A

B

C

D

A

B

D

 

 

 

 

 

主串

...

...

A

B

C

D

A

B

A

...

...

 

 

模式串

 

 

A

B

C

D

A

B

D

 

 

 

 

我们需要将模式串已匹配串的前缀  AB  与主串已匹配的后缀  AB 进行对齐,因为主串与模式串已匹配的串都是ABCDAB,所以这里可以理解为只需要将已匹配模式串的ABCDAB的前缀AB与后缀AB对齐即可,所以可以看出在求最大前缀后缀公共元素长度时我们只需要关注模式串就行了。注意我们的目的是为了让主串的指针不回退,所以必须要将模式串(已匹配部分)的前缀与后缀的最长公共元素部分进行对齐。
显然  模式串向右移动的位数 = 已匹配字符数 - 失配字符的上一位字符(已匹配字符)所对应的前缀后缀最长公共元素的长度值

所以只要求出模式串前缀后缀的最长公共元素长度就能找到下次开始扫描的位置,而对齐之后模式串的指针指向前缀后缀最长公共元素的下一个字符,这个字符的索引刚好就是已匹配部分字符串的前缀与后缀的最长公共元素的长度。

4 计算模式串前缀后缀最长公共元素长度

这里我们还以主串S "BBCABCDABABCDABCDABDE",和模式串P "ABCDABD",为例进行说明,为了便于理解我们用数组next[]用来存储模式串的前缀后缀最长公共元素长度,(这里需要说明,通常为了方便写代码数组next[]用来表示除当前字符外的最长相同前缀后缀长度,即next[]数组所有元素向右移动一位,并赋值next[0] = -1,为方便理解我这里就不做变形了)。
例如 next[m] 表示 模式串的子串P[0, m-1] 的前缀后缀的最长公共元素的长度为next[m],
即前缀 P0,P1,P2, ...... ,Pnext[m]-1与后缀Pm-next[m]+1,Pm-next[m]+2,Pm-next[m]+3,...... Pm 是相同串,长度为next[m]。
对于要求解的模式串的子串 [P0, Pj] 的前缀后缀的公共元素最大长度,设对与已知的[P0, Pj-1]的前缀后缀的最长公共元素长度为k,即前缀 P0,P1,P2, ...... ,Pk-1  与后缀 Pj-k,Pj-k+1,Pj-k+2,,,...... Pj-1 是相同串,就是说[P0, Pk-1] = [Pj-k, Pj-1],要求子串 [P0, Pj]的前缀后缀的最长公共元素长度显然有两种情况,
情况一:Pk = Pj  这时就会有[P0, Pk] = [Pj-k, Pj],next[j] = next[j-1] + 1,就是说[P0, Pk]的最长公共元素长度next[j] 比[P0, Pk-1] 的最长公共元素长度next[j-1]多1;
情况二:Pk ≠ Pj  我们需要在[P0, Pk) 左开右闭的区间内找到一个元素设为使得Px = Pj,如果找不到则next[j] = 0,注意这里隐含的条件就是Px 一定是在Pk的左侧寻找的,从而满足[P0, Px] = [Pj-x, Pj],我们在寻找的这个元素的时候因为要找最长的前后缀公共元素,当然用最笨的办法就是依次拿Pk-1  Pk-2  Pk-3  Pk-4  ......依次与Pj进行比较,看下是否满足[P0, Px] = [Pj-x, Pj],这样找一定能找到前后缀最长的公共元素长度,这种方法显然效率有点低。


更好的解决方法是要充分利用[P0, Pk-1] = [Pj-k, Pj-1]这个条件,如上图所示,我们要找的Px位置即Px-1的下一个元素,要保证[P0, Px]最长,就说[P0, Px-1]最长,即[P0, Px-1] = [ Px-1,Pj-1]是最长的,显然若[P0, Px-1]最长,则[P0, Px-1]是[P0, Pk-1]的前缀后缀的最长公共元素的前缀,通俗的说就是前缀的最长前缀,我们已知[P0, Pk-1]的前后缀的公共元素的最长值为next[k-1],所以索引x-1为最大公共元素长度next[k-1] 减去1,而Px-1的下一个元素Px 显然就是Pnext[k-1],我们需要用这个Pnext[k-1]与Pj进行比较,如果Pnext[k-1] = Pj,则next[k-1]就是我们要找的x值([P0, Px-1] + Px = [ Px-1,Pj-1] + Pj),如果不相等,我们就需要找[P0, Pnext[k-1]]的前缀,再次进行比较,即前缀[P0, Pk-1]的最大共公共元素前缀的最大公公共元素前缀(即前缀的前缀的前缀),依次类推,直到找到合适的位置。

5代码实现
 

      public static void main(String[] args) {
		String str1 = "BBC ABCDAB ABCDABCDABDE";
 		String str2 =  "ABCDABCDABDE";
		String str3 = "BBC";
		String str4 = "ABCDABEAB";
		String pattern = "ABCDABD";
		System.out.println(kmp(str1, pattern));//  15  终止时i=15
 		System.out.println(kmp(str2, pattern));//  4    终止时i=4
 		System.out.println(kmp(str3, pattern));// -1    终止时i=0
 		System.out.println(kmp(str4, pattern));// -1    终止时i=6
	}

	public static int kmp(String S, String P){
		int[] next = getNext(P);// 求next数组
		int matchSize = 0;// 已经匹匹配的长度
		int i = 0, j = 0;
		while (i < S.length() && S.length() - i >= P.length() - j) {// 注意终止条件
			if (S.charAt(i) == P.charAt(j) && j < P.length()) {
				matchSize++;//已经匹配的长度
				i++;
				j++;
			}else{
				if(j==0){
					i++;// 移动主串
					matchSize=0;// 已匹配长度清零
				}else{
					// 保持主串指针不动,移动模式串到合适位置
					matchSize=next[j-1];// 重置已匹配的长度
					j = next[j-1];// 注意这里要取Pj-1 的最长前缀长度,因为Pj是不匹配字符
				}
			}
			if(matchSize == P.length()){// 已匹配的长度等于模式串的长度了
				return i-P.length();
			}
		}
		System.out.println("i = " + i);
		return -1;
	}
	
	public static int[] getNext(String P) {
		//生成next数组
		int[]next = new int[P.length()];
		int j = 1;
		int k = 0;
		while (j < P.length() && k >= 0) {
			if (P.charAt(j) == P.charAt(k)) {
				next[j] = ++k;
				j++;// 找到了最大长度的前后缀公共元素
			} else {
				if (k >= 1) {
					k = next[k - 1];// 取前缀的最大前缀
				} else {
					// 没找到前缀后缀的公共元素最大长度
					k=0;
					j++;
				}
			}
		}
		System.out.println(Arrays.toString(next));
		return next;
	}

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值