如何理解KMP算法背后的思想?
首先,与暴力破解相比,KMP算法是一种更为高效的字符串匹配算法。那么,如何理解KMP算法背后的思想,为什么它可以比暴力破解更为高效呢?
leetcode第28题 :给你两个字符串 haystack 和 needle ,请你在 haystack 字符串中找出 needle 字符串出现的第一个位置(下标从 0 开始)。如果不存在,则返回 -1 。暴力破解:时间复杂度为 O(haystackSize*needleSize),也就是 O(N * M)。
class Solution {
public int strStr ( String haystack, String needle) {
int haystackSize = haystack. length ( ) ;
int needleSize = needle. length ( ) ;
if ( needleSize == 0 ) {
return 0 ;
}
if ( haystackSize < needleSize) {
return - 1 ;
}
int haystackIndex = 0 , needleIndex = 0 ;
for ( int i = 0 ; i <= haystack. length ( ) - needleSize; i++ ) {
for ( int j = 0 ; j < needleSize; j++ ) {
haystackIndex = i + j;
needleIndex = j;
if ( haystack. charAt ( haystackIndex) != needle. charAt ( needleIndex) ) {
break ;
}
if ( needleIndex == needleSize - 1 ) {
return i;
}
}
}
return - 1 ;
}
}
在上述暴力破解的过程中,每一次主串haystack和模式串needle都是在重新开始比对。
比如:
主串 haystack = "abcdef" , 模式串 needle = "abg" ;
整个比对的过程如下:
第一趟:haystack 从 'a' 开始,needle 从 'a' 开始。当发现 "abcdef" 中 'c' 与 "abg" 的 'g' 不等,则进行第二趟对比。
第二趟:haystack 从 'b' 开始,needle 还是从 'a' 开始。当发现 "bcdef" 中 'b' 与 "abg" 的 'g' 不等,则进行第三趟对比。
第三趟:haystack 从 'c' 开始,needle 还是从 'a' 开始。当发现 "cdef" 中 'b' 与 "abg" 的 'g' 不等,则进行第四趟对比。
第四趟:haystack 从 'd' 开始,needle 还是从 'a' 开始。当发现 . . . . . .
上述暴力破解的过程,实际上就是穷举所有可能的过程,不过,我们如果进一步观察思考,可以发现,上述过程是可以优化的。首先,模式串 needle = “abg”; 有一个特点,就是每个字符都不同。 如果我们运用这一特点,其实第一趟比较完成,可以直接走到第三趟,也就是说,第二趟是可以被省略的,为什么呢?因为在第一趟的比较过程中,当发现 “abcdef” 中 ‘c’ 与 “abg” 的 ‘g’ 不等 的过程中,隐藏了一个信息,那就是:“abcdef” 中的 “ab” 与 “abg” 的 “ab” 相同,而 “abg” 每个字符都不同,进而可以直接推断出模式串 “abg” 中的字符 ‘a’ 与 主串 “abcdef” 中的前两个字符 ‘a’ 和 ‘b’ 都不同,进而可以直接进行第三趟比较就好。 为了更好的理解,更进一步的,我们通过下面几种情况大体了解下,看看不同特征的模式串,如何加以利用,来优化减少每一趟的比较。
优化:s即haystack,为主串,p即needle,为模式串。
考虑几种情况:
第一种,s = "abcdef" ,p = "gh" ,那么,kmp算法退化为暴力求解过程。
第二种,s = "ghaghbghcghd" ,p = "ghi" ,那么,kmp算法优势不明显,但是与暴力破解还是有区别的,因为s的下标不回退,p的下标每次回退到初始位置0 。这里我们思考下,为什么s的下标可以不回退,而p的下标每次都要回退到初始位置0 呢?原因在于,我们利用next数组存储了p的一个关键信息,那就是,模式串p从始至终没有公共前后缀真子串(也就是各个字符都不相同),那么,当s与p进行比对的时候:
s = "gha" + "ghbghcghd" ,p = "ghi"
s走到字符a,p走到字符i,此时发现,字符a!= 字符i,如果是暴力破解方式,s的下标需要转移回退到h,p的下标需要转移回退到g,但是kmp显然不是这样,kmp中,s的下标保持不动(还是指向字符a),然后,p的下标回退到g,进一步直接和s的字符a进行比较。那么,问题又来了,为什么可以这样?
因为我们知道,p的各个字符都不相同,当s的字符a与p的字符i不等发生时,也意味着,在之前的对比过程中,s的gh与p的gh是相等的,所以呢,p中的i一定和p中的gh不等,那么也就一定和s中的gh不等,所以,s大佬,您下标别动,我小p直接从头开始就好。
综上所述,等等,我突然想到一句话,那就是,所有走过的路都不会白走,即便现在的解读你可能理解为这是弯路,但是未来决定过去,当有一天你会发现,原来所有的弯路,都会笔直的连接在一起,成就你独一无二的人生!抱歉,扯远了,但是你品,您细品,刚刚举的这个例子,是不是这么回事儿呢~
第三种,s = "aaacaabaacaaac" ,p = "aaaa" ,此时,p的next数组存储了一个关键信息,那就是p的所有字符都相同,都是字符a。那么,当s与p进行比对的时候:
s = "aaac" + "aaabaacaaac" ,p = "aaaa"
s走到字符c,p走到字符a(第4 个字符a),发现字符c!= 字符a,那么,此时我们保持s下标不动,那么p呢?此时p的最长公共真子串是aaa,但是此时p最应该移动到的位置是c的下一个字符(直接越过c),当模式串p比较特别的时候,比如,p = "aabaabaac" ,该如何最高效的计算next数组让匹配最高效呢?复旦大学朱洪教授对KMP串匹配算法进行了改进,其据说是针对此种情况的,同时,kmp也有一些特殊的扩展来响应一些特殊情况,但是万变不离其宗,本质还是借助next承载p的信息,进而借助走过的弯路,去开创更高效的未来,其实走来走去,走过的路都在p里~
第四种,s = "aacaabaacaac" ,p = "aacaac" ,那么,此时的kmp优势就显现出来了~
所以,KMP算法核心思想,其实是运用模式串的特征信息去简化暴力破解的各趟流程,其本质可以类比动态规划的思想 ,缓存中间步骤信息来减少执行次数。(比如abab是模式串,主串是abacabc,那么当模式串的第二次出现的b字符与主串的第一次出现的c字符相遇时,c与b不等,那么模式串直接回退到第一个a,因为对于模式串自身而言,ab与ab是相等的,而主串是无需回退的。)
KMP主体代码:
private int[ ] getNext ( String p ) {
int len = p. length ( ) , step= 0 ;
int[ ] next= new int [ len] ;
next[ 0 ] = 0 ;
for ( int i= 1 ; i< len; i++ ) {
while ( step> 0 && p. charAt ( step) != p. charAt ( i) ) {
step= next[ step- 1 ] ;
}
if ( p. charAt ( step) == p. charAt ( i) ) {
step++ ;
}
next[ i] = step;
}
return next;
}
private int KMP_MATCHER ( String T , String P ) {
int P_len= P . length ( ) ;
if ( P_len== 0 ) return 0 ;
int T_len= T . length ( ) ;
if ( T_len< P_len) return - 1 ;
if ( P_len== 1 ) {
for ( int i= 0 ; i< T_len; i++ ) {
if ( T . charAt ( i) == P . charAt ( 0 ) ) return i;
}
return - 1 ;
} ;
int[ ] next= getNext ( P ) ;
int step= 0 ;
for ( int i= - 1 ; i< T_len- 1 ; i++ ) {
while ( step> 0 && P . charAt ( step) != T . charAt ( i+ 1 ) ) {
step= next[ step- 1 ] ;
}
if ( P . charAt ( step) == T . charAt ( i+ 1 ) ) step++ ;
if ( step== P_len) {
System. out. println ( "Pattern occurs with shift" + ( i- P_len+ 2 ) ) ;
step= 0 ;
return i- P_len+ 2 ;
}
}
return - 1 ;
}
KMP题解:
class Solution {
public int strStr ( String haystack, String needle) {
return KMP_MATCHER ( haystack, needle) ;
}
private int [ ] getNext ( String p) {
int len = p. length ( ) , step = 0 ;
int [ ] next = new int [ len] ;
next[ 0 ] = 0 ;
for ( int i = 1 ; i < len; i++ ) {
while ( step > 0 && p. charAt ( step) != p. charAt ( i) ) {
step = next[ step - 1 ] ;
}
if ( p. charAt ( step) == p. charAt ( i) ) {
step++ ;
}
next[ i] = step;
}
return next;
}
private int KMP_MATCHER ( String T , String P ) {
int P_len = P . length ( ) ;
if ( P_len == 0 ) {
return 0 ;
}
int T_len = T . length ( ) ;
if ( T_len < P_len ) {
return - 1 ;
}
if ( P_len == 1 ) {
for ( int i = 0 ; i < T_len ; i++ ) {
if ( T . charAt ( i) == P . charAt ( 0 ) ) {
return i;
}
}
return - 1 ;
}
int [ ] next = getNext ( P ) ;
int step = 0 ;
for ( int i = - 1 ; i < T_len - 1 ; i++ ) {
while ( step > 0 && P . charAt ( step) != T . charAt ( i + 1 ) ) {
step = next[ step- 1 ] ;
}
if ( P . charAt ( step) == T . charAt ( i + 1 ) ) {
step++ ;
}
if ( step == P_len ) {
step = 0 ;
return i - P_len + 2 ;
}
}
return - 1 ;
}
private void printArray ( int [ ] array) {
for ( int i = 0 ; i < array. length; i++ ) {
System . out. print ( array[ i] + " " ) ;
}
System . out. println ( ) ;
}
}