KMP算法理解

参考这篇博客http://blog.csdn.net/qq_33323162/article/details/52397814

加了点文字,有助于自己理解







程序员代码面试指南(左程云)读书笔记
 第九章
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算法,该算法是由Donald Knuth,Vaughan Pratt和James H.Morris于1977年联合发明。
首先我们来看一下这个题的普通解法:
从左到右遍历str的每一个字符,然后看如果以当前字符作为第一个字符出发是否匹配match。比如:str=”aaaaaaaaaaaaaaaaaaaaaaaaaaaaaab”,match=”aaaab”。从str[0]出发开始匹配,匹配到str[4]=“a”时发现和match[4]=“b”不匹配,匹配失败。继续从str[1]=”a”开始匹配,匹配到str[5]=”a”时发现和match[4]=”b”不匹配,匹配失败。继续匹配。。。。直到匹配成功。这个方法的时间复杂度为O(N*M).
之所以时间复杂度为O(N*M)是因为每次遍历到一个字符时,只要发现不匹配,就要从头开始。之前的遍历检查不能优化当前的遍历。

KMP算法优化过程:
1,首先生成match字符串的nextArr数组,这个数字的长度与match字符串的长度一样。nextArr[i]的含义是在match[i]之前的字符串match[0…..i-1]中,必须以match[i-1]结尾的后缀子串(不能包含match[0])与必须以match[0]开头的前缀子串(不能包含match[i-1])最大匹配长度是多少。这个长度就是nextArr[i]的值。
例如:
match=”aaaab”字符串 nextArr[4]的值该是多少呢? match[4]=”b”,所以它之前的字符串为“aaa”,根据定义,这个字符串的后缀子串和前缀子串最大匹配为“aaa”。也就是当后缀子串等于match[1..3]=”aaa”,前缀子串等于match[0..2]=”aaa”时,这时前缀和后缀不仅相等,而且是所有前缀和后缀的可能性中最大的匹配。所有nextArr[4]的值等于3。
再例如:
match=”abc1abc1″
nextArr[7]=?
match[7]=”1″,它之前的字符串为“abc1abc”
根据定义:前缀:match[0..2]=“abc”
后缀:match[4..6]=“abc”
所以nextArr[7]=3.
关于如何等到nextArr数组的呢?我们把KMP算法大概过程介绍之后再来介绍。接下来我们看看nextArr是怎样在整个过程中起到作用的呢?
2,假设从str[i]字符出发,匹配到 j 位置的字符发现与match中的字符不匹配。也就是说str[i]与match[0]一样,并且从这个位置开始一直可以匹配,即:str[i…j-1]与match[0…j-i-1]匹配,直到发现str[j]!=macth[j-i]。匹配停止,如下图所示:《KMP算法理解》
因为现在已经有了match字符串的nextArr数组,nextArr[j-i]的值表示match[0…j-i]这一段字符串前缀与后缀的最长匹配,假设是图2中的a区域这一段,后缀是图2中的b区域这一段。再假设a区域的下一个字符为match[k],如下图所示:《KMP算法理解》 图2
那么下一次匹配检查不再像普通解法那样退回到str[i+1]位置从新开始与match[0]匹配,而是直接让str[j]与match[k]进行匹配。如下图3所示:《KMP算法理解》
图3
在图3中,str中要匹配的位置仍然是 j ,而不是退回。对match来说,相当于向右移动,让match[k]移动到与str[j]同一个位置上,然后进行后续的匹配。直到在str的某个位置完全匹配出match,说明str中有match,如果match移动到最后也没有匹配出来,说明str中没有match。那么为什么这么做是正确的呢?如图4所示:《KMP算法理解》
图4
在图4中,匹配到A字符才发生的不匹配,所有c区域等于b区域,b区域又与a区域相等(因为nextArr的含义如此),所以c区域和a区域不需要检查,必然相等。所以直接把字符C滑动到A的位置直接开始检查即可。其实这个过程相当于是从str的c区域的第一个字符从新开的始匹配过程(c区域的第一个字符和match[0]匹配,并向右的过程),只不过因为c区域与a区域一定相等,所以省去了这个区域的匹配检查,直接从字符A和字符C往后继续匹配检查
那为什么开始的字符从str[i]直接跳到c区域的第一个字符呢?中间的为啥不用检查匹配呢?因为在这个区域中,从任何字符出发都不能匹配出match,下面用图5解释说明一下:《KMP算法理解》
图5
注意:我们到图5 的这块分析,是在图4中从a区域的下一个字符C直接与str中c区域的A进行比较,这就省了一些比较。那为什么呢?为什么从C和A开始比较呢?难道他们之前的区域就不可能匹配上吗?会不会有遗漏呢?好,假设有遗漏吧?那么就假设之前区域中有个位置k,从k出发与match[0]开始比较,能够从str中得到一个完整的match,从图4中我们知道,比较到str的B和match的A的时候,发现不能匹配上;但是现在我们假设在图5的k位置可以匹配所有的match,那么在图5中的str中有个以k开头到A的d区域,可以和match中等长度的e区域匹配上,因为我们假设了从k开始于match[0]匹配可以将match全部找到;那么从图4中我们又可以知道,我们图4过程中是从str[i]与match[0]开始比较,一直到A和B不匹配,那么可知str[i]到A之前的区域都和match[0]到B之前的区域是相等的;那么由于k位置在这个区域内部,所以看图5,在match中肯定有d’与str的d区域是相等的,因为图4中A之前,B之前的两个区域相等,所以d和d’对应下来肯定相等。那么由于d=e且d=d’,可以知道在match中e=d’,那么回到图4或图5中的match的B位置,可以看出e区域和d’区域其实是A最大前缀和最大后缀;而我们已经求得B的最大前缀和最大后缀是a区域和b区域,而由于k位置在中间,可以知道d区域大于c区域,也就是说显然e区域也是大于a区域和b区域,那么此时的假设前提得到e区域应该是match的B位置的最大前缀,但实际上B的最大前缀是a,所以我们的此时是矛盾的,所以此时假设不成立,所以不存在一个k位置,可以得到完整的match,所以,我们可以直接跳到C位置直接与A位置相比较。


在图5中,假设d区域开始的字符是“不用检查”区域的其中一个位置,如果从这个位置开始能够匹配出match,那么毫无疑问,起码整个d区域应该和从match[0]开始的e区域匹配,即d区域与e区域长度一样,且两个区域字符都相等。同时我们注意到,d区域比c区域大,e区域比a区域大。如果这样的情况发生了,假设d区域对应到match字符串是d`区域,也就是字符B之前的字符串的后缀,而e区域本身就是match的前缀,所以对match来说,相当于找到了B这个字符之前的字符串(match[0…j-i-1])的一个更大的前缀与后缀匹配,一个比a区域和b区域更大的前缀和后缀匹配,e区域和d`区域。这与nextArr[j-i]的值是自相矛盾的,因为nextArr[j-i]的值代表的就是match[0…j-i-1]字符串上最大的前缀与后缀匹配长度,所以根本不会有更大的d`区域和e区域,d区域和e区域也不会相等。

最后我们来解释如何快速得到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]字符为图6中的A字符,match[i-1]为图6中的B字符,如图所示:《KMP算法理解》
图6
通过nextArr[i-1]的值可以知道B字符前的字符串的最长前缀和与后缀匹配区域,图6中L区域为最长的前缀子串,K区域为最长匹配的后缀子串,图6中字符C为L区域后的字符,然后看字符C与字符B是否相等。

2,如果字符C与字符B相等,那么A字符之前的字符串的最长前缀与后缀匹配区域就可以确定。前缀子串味L区域+c字符,后缀为K区域+B字符,即:nextArr[i]=nextArr[i-1]+1.

3,如果字符C与字符B不相等,就看字符C之前的前缀与后缀匹配情况,假设字符C是第cn个字符(match[cn]),那么nextArr[cn]就是其最长前缀和后缀匹配长度,(注意:因为每跳到一个位置cn,nextArr[cn]的意义就表示它之前的字符串的最长匹配长度)如图7所示:《KMP算法理解》
图7
在图7中,m区域和n区域分别是字符C之前的字符串的最长匹配的后缀与前缀区域,这是通过nextArr[cn]的值确定的,当然两个区域是相等的,m`区域为k区域最右的区域且长度与m区域一样,因为k区域和l区域是相等的。所以m区域和m`区域也想等,字符D为n区域之后的一个字符,接下来比较字符D是否与字符B相等。
1)如果相等,A字符之前的字符串的最长前缀与后缀匹配区域可以确定。前缀子串为n区域+D字符,后缀为m`区域加+B字符,子令nextArr[i]=nextArr[cn]+1.
2)如果不等,继续往前跳到字符D,之后过程与C类似,一直这样的过程,跳的每一步都会有一个新的字符与B比较(就像C字符和D字符一样),只要有相等的情况,nextAii[i]的值就能确定。
4,如果向前跳到最左位置(即match[0]的位置),此时nextArr[0]==-1,说明字符A之前的字符串不存在前缀和后缀匹配的情况,则令nextArr[i]=0,这样不断向前跳的方式可以正确的算出nextArr[i]值得原因还是因为每跳到一个位置cn,nextArr[cn]的意义就表示它之前的字符串的最长匹配长度

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;
		int cn = 0;		//跳到的位置
		while (pos < next.length) {
			//分支中有next[pos++]就说明pos位置已经求好;否则就是条件2,(cn>0)继续跳
			if (ms[pos - 1] == ms[cn]) {
				next[pos++] = ++cn;//前缀就是前cn个加上当前cn位置,也就是++cn的值,表示个数
			} else if (cn > 0) {//还可以往前跳,继续跳的位置就是上一次跳的位置的前缀的后一位值,也就是跳到next[cn]位置
				cn = next[cn];
			} else {//知道跳到最左边,不能再跳了
				next[pos++] = 0;
			}
		}
		return next;
	}

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