牛客堂刷题之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算法是:提前算好了next数组,然后按照next数组中的一些规律来减少我们匹配的复杂度

next数组表示的是当前元素 i 以前的最长前缀和最长后缀,
match串: 1  2  3 1  2 b
next数组:-1 0  0  0 1 2
1位置的时候,开始默认设置为-1
2位置的时候,以前的位置只有一个1,所以是0
3位置的时候,以前的是 1 2,所以还是0
1位置的时候,以前的是1 2 3,所以还是0
2位置的时候,以前是1 2 3 1,所以前缀是1,后缀是1,所以最长子串的长度是1
b位置,以前是1 2 3 1 2,所以最前缀是1 2,最后缀是1 2,两者相等,所以长度为2
类似的还有
match:     a a a a a b
next数组:-1 0 1 2 3 4 
我们先假设已经得到了next数组,先来看看如何匹配
牛客堂刷题之KMP_第1张图片
如图,假设str串和match串匹配到 i 和 j 处无法继续匹配,那么我们利用已经计算出的next数组
可以获取match串 j 位置的最长前缀 C和最长后缀B,A是和B相等的原串的一部分,显然C=A=B
所以下一次比较的时候可以直接比较元素E与str[i],也就是match串向右移动。

比如说原串是1 2 3 1 2 1 2 3 4 6 7,匹配串是1 2 3 1 2 b,next数组是 -1 0  0  0 1 2
首先匹配是

1 2 3 1 2 1 2 3  4 6 7
1 2 3 1 2 b

当比较到1和b的位置时,无法继续匹配
此时next[b]的值是2,那么 右移后的结果就是
1 2 3 1 2 1 2 3  4 6 7
          1 2 3 1 2 b
此时再继续比较即可。

刚才图中我们省略了一部分,有一部分不用比较可以直接略去
如图所示
牛客堂刷题之KMP_第2张图片
紫色横线这一部分可以直接省去,那么为什么可以省去呢?
我们先假设在紫色横线内找到了和match全部匹配的,假设是d和e全部匹配。
又因为str和match是匹配到最后一个才不相等,那么d和d‘也是相等的,所以d’和e就是相等的
那么d‘作为 j 位置的最后缀,e为j位置的最前缀,很明显a不等于e,d’不等于B,所以就与next计算出的最长前后缀产生矛盾,所以就不用比较这一部分的值


匹配完了,我们再继续看如何得出next数组
牛客堂刷题之KMP_第3张图片
如图,我们要计算i位置的next值,假设已经比较到了A和B,L和K是已经全部相等的,
如果A和B相等,那么i位置的next值等于 i-1位置处的next值加1
如果A和B不相等,由于A的最长前缀 x 和后缀 y 我们已经计算出,显然y‘和y是相等的(都是l和k的末尾处)
所以我们只需要比较e和B是否相等即可。

代码:

package com.cowcode_fourth;

public class KmpTest {

	public static void main(String[] args) {
		 
		String str = "1231234";
		String ma = "1234121ab";
	 
		System.out.println(getIndexOf(str,ma));
	}
 
	
	 public static int getIndexOf(String s, String m) { 
	        if (s == null || m == null || m.length() < 1 || s.length() < m.length()) { 
	        return -1; 
	        } 
	        char[] ss = s.toCharArray(); 
	        char[] ms = m.toCharArray(); 
	        int si = 0; 
	        int mi = 0; 
	        int[] next = getNextArray(ms); 
	        while (si < ss.length && mi < ms.length) { 
	        if (ss[si] == ms[mi]) { 
	            si++;   //相等都加1
	            mi++; 
	        } else if (next[mi] == -1) { 
	            si++; 
	        } else { 
	            mi = next[mi]; //可以看做是向右滑动的过程
	        } 
	        } 
	        return mi == ms.length ? si - mi : -1; //如果找到,那么match串也就走完,直接和长度比较
	    } 


	    public static int[] getNextArray(char[] ms) { 
	        if (ms.length == 1) { 
	            return new int[] { -1 }; 
	        } 
	        int[] next = new int[ms.length]; 
	        next[0] = -1; 
	        next[1] = 0; 
	        int pos = 2; //要算的next元素的值
	        int cn = 0;  //可以看做是进位,相等了就向前进一位
	        while (pos < next.length) { 
	            if (ms[pos - 1] == ms[cn]) { 
	                next[pos++] = ++cn; 
	            } else if (cn > 0) { 
	                cn = next[cn];  //用cn的最长前缀值和其比较
	            } else { 
	                next[pos++] = 0; 
	            } 
	        } 
	        return next; 
	    }

}


复杂度:

匹配过程中,match不断向右移动,str匹配的位置时不退回的,所以最坏情况下,match向右滑动n个单位,所以时间复杂度为O(n)
计算next数组过程中,我们关注pos和pos-cn两个变量和求解next数组的代码
pos最小是0,最大是m,cn不可能大于m,所以有
                                           pos(0~~m)           pos-cn(0~~~m)
第一个循环分支:              增加                           不变
第二个循环分支:              不变                            增加
第三个循环分支:              增加                             增加

因为pos+pos-cn<=2m,所以循环总次数不可能大于2m,所以时间复杂度为O(m)
所以总体的复杂度是O(m)+O(n)
又因为KMP发生的条件是m<n,所以时间复杂度是O(n)


你可能感兴趣的:(算法,KMP)