算法拾遗三十KMP算法

算法拾遗三十KMP算法

      • KMP算法
        • 前缀关系证明
      • KMP的Code

KMP算法

算法拾遗三十KMP算法_第1张图片
暴力解:O(N*M)的复杂度
KMP:
先求前缀与后缀串的最长匹配长度:
算法拾遗三十KMP算法_第2张图片
6位置的信息求法:与abcabc字符串有关
长度为1的时候:
前缀:a
后缀:c
不相等
长度为2的时候:
前缀:ab
后缀:bc
不相等
长度为3的时候:
前缀:abc
后缀:abc
相等
长度为4的时候:
前缀:abca
后缀:cabc
长度为5的时候:
前缀:abcab
后缀:bcabc
前缀串不能取到整体后缀串不能取到整体
如上3位置的时候是最长的,所以6位置的信息是3
算法拾遗三十KMP算法_第3张图片
算法拾遗三十KMP算法_第4张图片
再来看如下数组
算法拾遗三十KMP算法_第5张图片
0位置人为规定-1
1位置由于不能取整体,所以1位置为0
算法拾遗三十KMP算法_第6张图片
2位置的前面字符串为aa,2位置为1
算法拾遗三十KMP算法_第7张图片

3位置前面是aab不存在匹配的前缀和后缀
算法拾遗三十KMP算法_第8张图片
aaba只有1,a和a想匹配
算法拾遗三十KMP算法_第9张图片
同理推后面的:
算法拾遗三十KMP算法_第10张图片
如上数组是next数组,
假设在s1里面去找s2,那么要求s2的next数组,那么去求的时候就可以加速。
假设s1是从i位置出发并和s2的0位置做匹配
算法拾遗三十KMP算法_第11张图片
且i位置的字符和0位置的字符配上了,当我来到s1的x位置的时候和s2的y位置没有配上,那么对于暴力解来说是我彻底放弃已匹配的串然后从s1的i+1位置和s2的0位置重新开始匹配。
在KMP算法里面:
S1从i位置出发,S2对着0位置出发在S1的x位置和S2的Y位置没有配上,对于S2的每一个位置我们求了个next数组,Y位置前面的最长前后缀串的长度我们是知道的。那么此时找到后缀串在s1中的对应位置j,
算法拾遗三十KMP算法_第12张图片
将s2从i位置推到s1的j位置再继续去匹配:只需要看S1的x位置及其后面的是否和S2的z位置及其后面的字符串匹配。
算法拾遗三十KMP算法_第13张图片
那么i到j位置中间的任何一个开头是否能配出S2呢?
不能配出S2,看如下例子:
算法拾遗三十KMP算法_第14张图片
然后s2往右推:
算法拾遗三十KMP算法_第15张图片
再往右推:得到a的信息是0,则用s2的0位置和s1的t位置比较,不相等,得到s2的0位置next数组值为-1,不能推了
算法拾遗三十KMP算法_第16张图片
整体流程:
算法拾遗三十KMP算法_第17张图片
第一次失败,扯出信息:最长前后缀长度为2,s2的0位置向右推:
算法拾遗三十KMP算法_第18张图片
t和b对齐了,得到前后缀匹配长度为1,得到j撇
算法拾遗三十KMP算法_第19张图片
发现又不等,只能继续往下推
算法拾遗三十KMP算法_第20张图片
发现a是0,没有前缀和后缀,0位置的a前后缀是-1,表明需要换个开头,宣告i位置到所有位置的开头都失败了,换个字符开头再做匹配
算法拾遗三十KMP算法_第21张图片
算法拾遗三十KMP算法_第22张图片
如上推导涉及两个加速,一个是不用验的一个加速画双横线的abcd一定互相相等,直接从z和a相等开始验证,直接舍弃以b,c,d开头的串。

前缀关系证明

证明
那么i到j位置中间的任何一个开头是否能配出S2呢?

在S1中从i位置开始一直到x配不上S2从0位置开始到Y的位置。
算法拾遗三十KMP算法_第23张图片
对于Y有个最长相等前缀和最长相等后缀,然后确认S1的j位置
算法拾遗三十KMP算法_第24张图片
需要证明i到j中间的k位置出发不可能配出S2出来。
1、假设从k位置出发能配出S2出来,那么从k位置到X前面的那一段是不是也能搞定S2等量的前缀?
2、这样是否有矛盾,K位置到X前面的那一段对应着S2的Y位置之前字符串等量的后缀,表示Y之前存在一个更长的等量的前缀和后缀和之前求的next相悖论了【表明这种情况不可能发生】。

KMP的Code

public class KMP {

	public static int getIndexOf(String s1, String s2) {
		if (s1 == null || s2 == null || s2.length() < 1 || s1.length() < s2.length()) {
			return -1;
		}
		char[] str1 = s1.toCharArray();
		char[] str2 = s2.toCharArray();
		//str1比对到的位置
		int x = 0;
		//str2比对到的位置
		int y = 0;
		// O(M) m <= n
		int[] next = getNextArray(str2);
		// O(N) 证明:已知while里面有三条分支,需要估计出三个分支中的次数
		//x是str1中的位置,x能取到最大值为str1的长度 N
		//y是str2中的位置,y能取到最大值为str2的长度 M
		//x-y 最大值为N(X最大为N y最小为0)
		//循环的第一个分支会推高x和y,x-y的量不变,x会上升
		//循环的第二个分支会推高x,x会上升 x-y会上升
		//循环的第三个分支会减小y,x不变 x-y会上升
		//三个循环的次数小于等于2N 时间复杂度O(N)
		while (x < str1.length && y < str2.length) {
			//如果配上了
			if (str1[x] == str2[y]) {
				x++;
				y++;
			} else if (next[y] == -1) { // y == 0位置
				x++;
			} else {
				//str2 的y位置跳转到nextY位置,Y往前跳减小
				y = next[y];
			}
		}
		//y越界的条件,str1的某一个开头路过了str2的所有,y没越界说明没有找到匹配的
		//从x出发向左推y的长度【x和y加的相同的长度】
		return y == str2.length ? x - y : -1;
	}

	public static int[] getNextArray(char[] str2) {
		//任何串0位置都是-1
		if (str2.length == 1) {
			return new int[] { -1 };
		}
		int[] next = new int[str2.length];
		next[0] = -1;
		next[1] = 0;
		int i = 2; // 目前在哪个位置上求next数组的值
		//前缀的下一个字符和当前位置的前一个字符(i-1位置的字符)比较
		int cn = 0; // 当前是哪个位置的值在和i-1位置的字符比较【next[i-1]不仅代表前后缀匹配串的长度
		//还代表前缀的下一个字符的位置 】
		//复杂度证明
		// i最大值 M  i-cn最大值M
		//第一个分支 i和cn同时加 i不变 i-cn不变
		//第二个分支 i不变 cn变小 i不变 i-cn在变大
		//第三个分支 i增加 i变大 i-cn变大
		//通过估计三种分支发生次数的极限来估计while分支的复杂度
		while (i < next.length) {
			if (str2[i - 1] == str2[cn]) {
				// 配成功的时候
				next[i++] = ++cn;
			} else if (cn > 0) {
				//如果没有配成功,cn大于0,表示cn还能往左跳
				cn = next[cn];
			} else {
				next[i++] = 0;
			}
		}
		return next;
	}

	// for test
	public static String getRandomString(int possibilities, int size) {
		char[] ans = new char[(int) (Math.random() * size) + 1];
		for (int i = 0; i < ans.length; i++) {
			ans[i] = (char) ((int) (Math.random() * possibilities) + 'a');
		}
		return String.valueOf(ans);
	}

	public static void main(String[] args) {
		int possibilities = 5;
		int strSize = 20;
		int matchSize = 5;
		int testTimes = 5000000;
		System.out.println("test begin");
		for (int i = 0; i < testTimes; i++) {
			String str = getRandomString(possibilities, strSize);
			String match = getRandomString(possibilities, matchSize);
			if (getIndexOf(str, match) != str.indexOf(match)) {
				System.out.println("Oops!");
			}
		}
		System.out.println("test finish");
	}

}

y = next[y]
如下例子说明

算法拾遗三十KMP算法_第25张图片
x会到t位置y会到b位置,而s2最后的b对应的最长前后缀匹配长度为2
y= next[y] = 2,所以s2的y又重新回到2位置的b上面。
算法拾遗三十KMP算法_第26张图片
等同于s2向右推到了s1的3位置再通过b和t比较

next数组如何快速求:
首先规定next数组0位置为-1,1位置为0,2位置的求法为0,1位置相等就是1,不相等就是0

如果next数组某个i位置需要求解,已知i位置前面的next数组的信息,那么如何推导i位置的信息?
已知i位置字符串的信息和它本身是没有任何关系的只和它前面的字符串信息有关系,假设i-1位置里面它的next信息是7,表示i-1位置之前的相同前后缀字符串长度为7。
算法拾遗三十KMP算法_第27张图片
那么我们要求i位置的信息我们只需要盯住i-1位置和图中问号位置。如果相等的话i位置的next信息就是8.
算法拾遗三十KMP算法_第28张图片
0到6位置和12到18位置的串相等,如果7位置上面的字符和19位置上面的字符相等那么20位置的next为8【注意:i位置的next值不可能超过i-1位置的next加1】
说明:
如果上图中20位置的next为9的话那么就表示存在
0-8, 11-19 互相匹配,这和原来19位置next等于7相悖论了,这样来说的话19位置的next应该为8。

如果7位置上的字符和19位置上的字符不匹配:
算法拾遗三十KMP算法_第29张图片
以如上图为例,要求k位置的next信息,找到b位置对应的前后缀串的信息发现前后缀对应的t和b不匹配,则需要左边方框往前跳,找到t对应的next信息。
算法拾遗三十KMP算法_第30张图片
t对应的信息是3,然后看前缀的后一个和后缀的后一个是否相等,相等则k位置的next是4

你可能感兴趣的:(算法,数据结构,java)