KMP算法是BF(Brute Force)算法的一种改进算法,什么是BF算法这里不多做解释。


1.KMP算法实现思路:


  每当一趟匹配过程中出现字符比较不等时,不需要回溯主串上面的指针 i,而是利用已经计算出的模式串P在 j位置前面的子串P 0...P j-1部分匹配值k将模式向右滑 j-k个字符,然后继续进行比较。


 


2.理解"前缀"、"后缀"和“部分匹配值”的概念:

  首先这里要引入"前缀"和"后缀"的概念(这个很重要),

  (1)前缀:指除了最后一个字符以外,一个字符串的全部头部组合;

  (2)后缀:指除了第一个字符以外,一个字符串的全部尾部组合;

部分匹配值:就是"前缀"和"后缀"的最长的共有元素的长度,如以字符串"ABCDABD"为例:

  -   "A"的前缀和后缀都为空集,共有元素的长度为0;

  - "AB"的前缀为[A],后缀为[B],共有元素的长度为0;

  - "ABC"的前缀为[A, AB],后缀为[BC, C],共有元素的长度0;

  - "ABCD"的前缀为[A, AB, ABC],后缀为[BCD, CD, D],共有元素的长度为0;

  - "ABCDA"的前缀为[A, AB, ABC, ABCD],后缀为[BCDA, CDA, DA, A],共有元素为"A",长度为1;

  - "ABCDAB"的前缀为[A, AB, ABC, ABCD, ABCDA],后缀为[BCDAB, CDAB, DAB, AB, B],共有元素为"AB",长度为2;

  - "ABCDABD"的前缀为[A, AB, ABC, ABCD, ABCDA, ABCDAB],后缀为[BCDABD, CDABD, DABD, ABD, BD, D],共有元素的长度为0。


3.下面开始具体解析KMP:


 


假设主串S的长度为n,模式串P的长度为m, i为主串S当前位置的指针, j为模式串P当前位置的指针:


S0 .....Si-jSi-j+1Si-j+2.......Si-2Si-1...........Sn


        P0 P1 P2...............Pj-2Pj-1 


即:Si-jSi-j+1Si-j+2...Si-1=P0 P1 P2...Pj-2Pj-1                      (1-1)

当Si!=Pji不动,模式串P向右移动多少个字符最正确(即要保证不会漏掉可能的匹配或不会重复不必要的匹配过程)

如果P本身的每一个字符都不相同,那么就可以直接将模式串P向右移动j个字符,道理很简单因为P0!=P1!=P2...!=Pj-1,由上面等式(1-1)可知P0也不等于Si-jSi-j+1Si-j+2.......Si-2Si-1中的任何一个,所以可以直接从P0开始和Si进行下一轮比较(指针i不需要回溯,指针j回溯到模式串的起始位置)。

但是如果模式串P存在很多重复的字符如:abcabcabd这种情况时就不能直接将j指针移动到P0了,例如主串为fffffabcabcabcabcabdfffff时

               i       

         fffffabcabcabcabcabdfffff

          abcabcabd

              j

              ↑ 发现 c != d 即 S!= Pj

此时应该怎么移动呢?如果直接将j移动到P0然后和Si比较则会出现漏掉匹配的情况即匹配结束后找不到匹配串,正确的做法是将j—>P5位置(相当于向右滑动3个位置)然后和Si继续比较,如下所示:

               i       

         fffffabcabcabcabcabdfffff

               abcabcabd

              j

为什么是移动到P5呢?这个P5是怎么来的?这个就是整个算法的关键点,理解了这一点也就理解了KMP算法的本质。

其实这个5就是Pj-1的部分匹配值k,移动字符个数=j-k=8-5=3(j=8,k=5)

根据上面字符串部分匹配值的定义可知当j=8时P0P1...Pj-1等于字符串abcabcab,该字符串的前缀和后缀的最长共有元素的长度为5,即abcabca和bcabcab重叠的部分最大长度为5。

那么这是什么原理呢?为什么P0P1...Pj-1的部分匹配值就是模式P在位置j失配时重新开始匹配的位置呢?为什么不需要回溯i指针及完全回溯j指针到P0,却不会出现漏掉匹配或者怎么能确保这种情况下是没有进行不必要的重复匹配呢?

下面去看分析:

当在j位置失配时有 P!= S且等式 Si-jSi-j+1Si-j+2...Si-1=P0 P1 P2...Pj-2Pj-1 必定成立

又由字符串部分匹配值的定义可知P0P1...Pk-1=Pj-kPj-k+1...Pj-1,上面的列子中即P0P1P2P3P4=P3P4P5P6P7(j=8,k=5)

因为:Pj-kPj-k+1...Pj-1=Si-kSi-k+1...Si-1,所以P0P1...Pk-1=Si-kSi-k+1...Si-1

前缀和后缀的最长共有元素的意思就是说当y>k时不可能存在Pj-yPj-y+1...Pj-1=P0P1P2...Pj-y-1(这里是关键,y就是该字符串的某一个前缀和后缀的长度,k是该字符串的部分匹配值,所以不可能存在一个y>k使得等式成立),只有当y=<k时等式才会成立;因此可以推断出:

P0P1P2...Pj-y-1和Si-j+1Si-j+2Si-j+3...Si-1进行匹配时前面j-k次都不会匹配成功,这就是KMP算法中当失配时直接将模式串P向右滑动k个字符的原理。

模式串P的部分匹配值表怎么求,下篇博文里面再详细说明,其实关键点还是前缀和后缀以及部分匹配值的问题,把这个搞懂了就都懂了。



具体实现:

public class KMP {

	void getNext(String pattern, int next[]) {
		int j = 0;
		int k = -1;
		int len = pattern.length();
		next[0] = -1;

		while (j < len - 1) {
			if (k == -1 || pattern.charAt(k) == pattern.charAt(j)) {

				j++;
				k++;
				next[j] = k;
			} else {

				// 比较到第K个字符,说明p[0——k-1]字符串和p[j-k——j-1]字符串相等,而next[k]表示
				// p[0——k-1]的前缀和后缀的最长共有长度,所接下来可以直接比较p[next[k]]和p[j]
				k = next[k];
			}
		}

	}

	int kmp(String s, String pattern) {
		int i = 0;
		int j = 0;
		int slen = s.length();
		int plen = pattern.length();

		int[] next = new int[plen];

		getNext(pattern, next);

		while (i < slen && j < plen) {

			if (s.charAt(i) == pattern.charAt(j)) {
				i++;
				j++;
			} else {
				if (next[j] == -1) {
					i++;
					j = 0;
				} else {
					j = next[j];
				}

			}

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

	/**
	 * @param args
	 */
	public static void main(String[] args) {
		// TODO Auto-generated method stub
		KMP kmp = new KMP();
		String str = "abababdafdasabcfdfeaba";
		String pattern = "abc";
		System.out.println(kmp.kmp(str, pattern));
	}

}