KMP算法是在字符串匹配算法中比较绕的.主要是需要理解KMP种next数组求解的必要性以及j 的回溯依据; 在理解KMP 算法时, 很容易头秃. 这个算法可以多理解几次, 理解的过程中更加透彻;
KMP 算法也是比较著名的模式匹配算法. 是由D.E.Knuth,J.H.Morrs 和VR.Pratt.
发表的一个模式匹配算法. 可以大大避免重复遍历的情况;
例如,假设现在有一个主串S = “abcdefgab” ; 模式串 T = “abcdex” ; 如果使用暴风算法的话,前面5个字母完全相等,直到第6个字母.'f' 和 'x' 不相等; 如下图;
接下来按照爆发算法,我们需要执行以下这②③④的过程.即主串i=2,3,4,5,6时,首字母与子串T的首字母均不相等;
按照爆发算法的设计,我们的执行过程就是如此. 但是对于要匹配的子串T来说,"abcdex" 首字母"a" 与后面的串"bcdex"中任意一个字符都不相等;
也就是说, 既然"a"不与自己后面的子串中任何一个字符相等. 那么对于上图中①来说,前面5个字符分别相等.那就意味着子串T与首字符"a"不可能与S 串的第2位到第5位想的. 那么图②③④的判断都是多余的;
KMP算法的想法是,设法利用这个已知信息,不要把"搜索位置"移回已经比较过的位置,继续把它向后移,这样就提高了效率
==理解KMP 关键!==
T[6] != S[6]
,尽管我们已经知道T[1] != T[6]
. 但是不能断定 T[1] 一定不等于 s[6]. 因此需要保留第⑥步;例如,假设现在有一个主串S = “abcababca” ; 模式串 T = “abcdex” ;
既然 i 值不能回溯, 也不可以变小; 那么考虑的变化就是 j 值;
通过刚刚的分析了解,我们屡次提到了 T 串的首字符 与自身后面字符的比较; 发现如果有相等字符, j值的变化就会不相同; 也就是说, j 值的变化其实与主串的关系. 更多取决于T串中的结构是否有重复问题;
如上图,由于 T = "abcdex" 当中没有重复的数字, 所以 j 就由 6 变成了1;
而下图,由于 T = "abcabx". 前缀 "ab" 与最后的"x" 之前串的后缀 "ab" 是相等的. 所以就 j 就由 6 变成了 3;
因此得出规律: j 值的多少取决于当前字符之前的串前后缀相似度;
在例1的情况下, 模式串 T
中不存在任何重复的字符; 所以此时 next
数组,推演过程符合公式中第1种情况与第3种情况.因此 next数组为= {011111}
;
在这个例子中, 当 j = 1,2,3,4
时 与上一个例子无异;
当 j = 1
,属于情况1,则next[1] = 0
;
当 j = 2,3,4
时,属于情况3,则next[2...4] = {0111}
;
当 j = 5
时, j
由 1
到 j-1
范围有字符 "abca
", 此时前置"a
" 与 后缀"a
" 相等;
p[1,2-1]与p[5-2+1,5-1] 到P1 = P4. 所以k = 2, 此时 next[5] = 2
;当 j = 6
时, j
由 1
到 j-1
范围有字符 "abcab
", 此时前置"ab
" 与 后缀"ab
" 相等
p[1,3-1]与p[6-3+1,6-1]. 所以k = 3, 此时 next[6] = 3
;经验: 如果前后缀一个字符相等,K值是2; 两个字符相等是3; n个相等k值就是n+1;
根据之前总结的K的求取规则! 可以根据相等的字符,换算出K值;
同时需要注意的是:
next
数组是比较连续的前缀字符和后缀字符; 例如当j=6
时,字符串"ababa
", 最大前缀是"aba
",后缀也是"aba
".j = 7
时,字符串"ababaa
",此时前缀是"a
",后缀是"a
". 不要误以为是"ab
";在这样的情况下,需要注意刚刚发生的重叠情况就可以了.
计算子串T的next数组:求解next数组其实就是在求解字符串的回溯位置;
假设, 主串S = “abcababca” ; 模式串 T = “abcdex”
根据刚刚的公式的推演过程,我们其实非常情况next数组的应该是 011111
. 但是我们如何利用代码来求解出next数组了?
011111,意味着什么.
意味着其实模式串中abcdex 根本没有可以便于回溯的地址.也就是当主串与模式串不匹配时,都需要从第1的位置上重新比较;
比如 abcababca 比较 abcdex 当比较到第4个位置是发现不匹配. 而此时主串的索引不变;
模式串的索引j当时等于4. 而此时发现不匹配,则要进行回溯. 那么应该回溯到哪里了?
j = next[j] ; j = 1
; 也就是把abcbabca 的第4个字符与模式串的第1个字符串重新开始比较;
首先默认next[1] = 0
; 表示它需要从0开始遍历;
接下来设计2个索引下标 i = 0, j = 1
;
i 用于求解回溯的地址, 而j用于模式串的遍历
如果出现 i = 0
,就是表示此时在模式串中并没有出现它相同的字符, 需要记录此时的回溯地址地址为1; next[j] = 1
; 表示需要把从模式串的第一个字符开始比较;
如果T[i] == T[j]
表示此时已经出现了模式串中有重叠字符,则回溯地址next[j] = i
;
如果既没有出现 i = 0
,且 T[i] == T[j]
表示此时从[i,j]
这个范围没有出现回溯位置的调整,我们则把 i
回溯到next[i]
的位置;
这个过程我们通过图例来分析! 这是KMP算法.也是众多匹配算法中最为难以理解的算法;
j = 1
的情况下.也是就是我们要找到[0,1]
这个范围的字符 a 是否存储需要回溯的情况.i = 0
, 所以如果主串和模式串匹配是,遇到第2个字符就不相配. 那么就应该讲主串后移1位,并且与模式串重新的第1位开始重新匹配计算;i=0,j = 1
; 所以我们应该i++,j++;
next[j] = i
; 所以此时`next[2] = 1;j=2
时,发现字符不匹配.需要把控制模式串比较的下标索引回溯到1. 从第一个字符重新开始比较;i = 1,j = 2
; 明白此时在[0,2]范围内,没有可以回溯的合理位置;i
回退到next[1]
. 也就是 i = next[i];
i = 1,next[1] = 0
;也就是0的位置; 0就意味着我们需要下一个字符进行比较时,我们需要头开始比较;next[i]
等于其他位置,表示把i回溯到其他位置. 而只有i有可回溯的可能性才会回溯到其他位置.i = 0
, 那么就意味着如果需要从头开始比较;1
. 所以i++,j++
; 因为这是当j = 3
时, 如果不匹配需要回溯到开始的位置. 所以j 也是要➕➕ 的;next[j] = i; next[3] = 1
;i = 1, j = 3
; 那么我们要看看这个范围内是否出现回溯的可能性;T[i] != T[j], a != c
,所以此时应该回到开始的位置;i = next[i]
; 此时 i 等于0 .i = 0
,也就意味着.当j = 4
时, 是需要从头开始比较;i++,j++
,同时next[j] = i
;i = 1,j = 4
, next[4] = 1
;[1,4]
范围内看有没有可以减少回溯长度的位置T[i]!=T[j], a != d
, 所以此时表示当我们需要重头开始比较;i = next[i]
; 此时 i = next[i] ; i = 0;
i = 0
,也就意味着.当j = 5
时, 是需要从头开始比较;i++,j++
,同时next[j] = i
;i = 1,j = 5
, next[5] = 1
;[1,5]
范围内看有没有可以减少回溯长度的位置T[i]!=T[j], a != e
, 所以此时表示当我们需要重头开始比较;i = next[i]
; 此时 i = next[i] ; i = 0;
i = 0
,也就意味着.当j = 6
时, 是需要从头开始比较;i++,j++
,同时next[j] = i
;i = 1,j = 6
, next[6] = 1
;j = 6
. 循环条件不能满足,则退出循环;在求解next
数组的4种情况:
next[1] = 0;
i=0
时,表示当前的比应该从头开始.则i++,j++,next[j] = i
;T[i] == T[j]
表示2个字符相等,则i++,j++
.同时next[j] = i
;T[i] != T[j]
表示不相等,则需要将i 退回到合理的位置. 则 i = next[i]
;那么 next
数组如何应用到KMP 的查找中了?
当字符比较不匹配时, 如果是暴力查找法时;
当发现主串与模式串在 i = 4,j = 4
时不匹配. 那么下一次比较
则是i = 2,j = 1
,重新开始比较;
当使用 KMP
时, i = 4, j = 4
时发现不匹配,则进行下一次比较时.
i
保留 4
的位置,不需要回退; 同时 j = next[j];
此时发现 next[j] = 1
,那么则让 j = 1.开始新的一轮比较'
//----KMP 模式匹配算法--- //1.通过计算返回子串T的next数组; //注意字符串T[0]中是存储的字符串长度; 真正的字符内容从T[1]开始; void get_next(String T,int *next){ int i,j; j = 1; i = 0; next[1] = 0; //abcdex //遍历T模式串, 此时T[0]为模式串T的长度; printf("length = %d\n",T[0]); while (j < T[0]) { printf("i = %d j = %d\n",i,j); if(i ==0 || T[i] == T[j]){ //T[i] 表示后缀的单个字符; //T[j] 表示前缀的单个字符; ++i; ++j; next[j] = i; printf("next[%d]=%d\n",j,next[j]); }else { //如果字符不相同,则i值回溯; i = next[i]; } } } 复制代码
int count = 0; //KMP 匹配算法 //返回子串T在主串S中第pos个字符之后的位置, 如不存在则返回0; int Index_KMP(String S,String T,int pos){ //i 是主串当前位置的下标准,j是模式串当前位置的下标准 int i = pos; int j = 1; //定义一个空的next数组; int next[MAXSIZE]; //对T串进行分析,得到next数组; get_next(T, next); count = 0; //注意: T[0] 和 S[0] 存储的是字符串T与字符串S的长度; //若i小于S长度并且j小于T的长度是循环继续; while (i <= S[0] && j <= T[0]) { //如果两字母相等则继续,并且j++,i++ if(j == 0 || S[i] == T[j]){ i++; j++; }else{ //如果不匹配时,j回退到合适的位置,i值不变; j = next[j]; } } if (j > T[0]) { return i-T[0]; }else{ return 0; } } 复制代码
KMP 算法也是有缺陷的. 比如,如果我们的主串 S = "aaaabcde"
,模式串T = "aaaaax"
. 其中 next
数组就是 012345
;
i = 5, j = 5
时, 我们发现字符 "b"
与 字符 "a"
不相等时, 如图① . 因此 j = next[5] = 4
."b"
与 第4个位置上的 "a"
依然不等; j = next[4] = 3;
如图③j = next[1] = 0
时,根据算法,此时 i++,j++
; 得到i = 6,j = 1
. 如图⑥;其实,在比较的过程发现 当中②③④⑤步骤的回溯比较都是多余的判断;
由于 T 串的第二,三,四,五位置的字符都与首位 "a"
相等, 那么可以用 首位 next[1]
的值去取代与它相等的字符后续的 next[j]
值. 那么我们来对 next
函数来进行改良;
next
数组= {0,1,2,3,4,5}
如果要节省刚刚 ②③④⑤ 的无效比较则 需要next 数组={0,0,0,0,0,5}
当 j = 1,nextVal = 0
; 继续保持next[1]
的逻辑;
当 j = 2
时, 也就是当j = 2
发生匹配错误, 那么由于第二个字符'b'
的 next
值是1, 而且第一个字符是'a'
.两者不相等.所以nextval[2] = next[2] = 1;
j = 3
时, 此时因为第 3
个字符 'a'
的 next
值是1, 所以得知 第1位的'a'
与 第3位的'a'
是相等,则此时nextVal[3] = nextVal[1] = 0;
j = 4
时, 因为第 4 个字符 "b"
的next = 2
; 所以可以得知 它与第 2 位字符 "b"
是相等,则nextVal[4] = nextVal[2] = 1;
j = 5
时,next
值为3 , 第5个字符”a”
与第3个字符”a”
相等,则nextVal[5] = nextVal[3] = 0
;j = 6
时,next
值为4 , 第6个字符”a”
与第4个字符”b”
不相等,则nextVal[6] = 4;
j = 7
时,next
值为2 , 第7个字符”a”
与第2个字符”b”
不相等,则nextVal[7] = 2;
j = 8
时,next
值为2 , 第8个字符”b”
与第2个字符”b”
相等,则nextVal[6] = nextVal[2] = 1
;j = 9
时, next
值为3, 第9个字符”a”
与第3个字符”a”
相等,则nextVal[9] = nextVal[3] = 0;
在求解nextVal
数组的5种情况:
nextval[1] = 0
;T[i] == T[j]
且++i,++j
后 T[i]
依旧等于 T[j]
则 nextval[i] = nextval[j]
i = 0
, 表示从头开始i++,j++
后,且T[i] != T[j] 则nextVal = j
;T[i] == T[j]
且++i,++j
后 T[i] != T[j] ,则nextVal = j;
T[i] != T[j]
表示不相等,则需要将i 退回到合理的位置. 则 i = next[i];
void get_nextVal(String T,int *nextVal){ int i,j; i = 1; j = 0; nextVal[1] = 0; while (i < T[0]) { if (j == 0 || T[i] == T[j]) { ++j; ++i; //如果当前字符与前缀不同,则当前的j为nextVal 在i的位置的值 if(T[i] != T[j]) nextVal[i] = j; else //如果当前字符与前缀相同,则将前缀的nextVal 值赋值给nextVal 在i的位置 nextVal[i] = nextVal[j]; }else{ j = nextVal[j]; } } } 复制代码