KMP算法的主要应用场景:
在一段(主字符串)中查找是否包含某一段(子串)
举例朴素模式:
在了解KMP之前,可以想象使用最简单的方法就是,子串与主串挨个对比,当子串某一位字符对比主串字符失败后,
主串将对比的位置回溯到上一次(开始的位置+1)。
子串将对比的位置回溯到0
一直到匹配成功,或者遍历完所有字符。
对于正常的字符串模式匹配,主串长度为m,子串为n,时间复杂度会到达O(m*n)。
图例 :
S串:abcdefgab
T串:abcdex
分析:
对于要匹配的子串T来说,"abcdex" 首字母a与后面的任意一个字符都是不相等的,既然首字母a不与后面的任意字符相等 (并且我们通过对比图中,已知主串的2-5位与子串的2-5位是判断相等的),那么也就意味着首字母a不可能与主串2-5的位置相等,那么朴素算法的2️⃣3️⃣4️⃣5️⃣判断对比都是多余的。
示例二:
另一种情况,如果T串后面含有与首字母"a"的字符
S = "abcabcabc"
T= "abcabx"
使用朴素算法进行对比
分析:
1,根据示例一的分析T串的首字母"a"与自己后面的第二位,第三位字符都不相等,所以2️⃣3️⃣判定为多余的对比。
2,然后发现T的首字母"a"与自己第四位字符"a"相等,第二位"b"与第五位"b"相等,观察1️⃣发现,T串的第四位"a",第五位"b" 与 S主串中相应的位置比较过为相等。因此可以判断出T的首字符"a",第二位字符"b" 与 主串S的第四位,第五位也是相等的,所以该步骤也可以忽略。
3,也就是说,T的子串中有,与首字母相等的字符,也可以省略一部分不必要的对比判断。
4,对比以上两个例子,发现朴素匹配法主串的(角标i值),在不断的回溯,而通过对比的规律发现这些回溯是可以避免发生的。
5,通过两个示例分析发现,S串,与T串对比时,只需要关注子串(下标值K的变化)
通过示例二分析发现,可以根据T串的结构中是否有重复字符的出现,从而决定K值的变化
KMP算法能将时间复杂度降低到O(m+n)
KMP算法的核心之一(next[]数组)
next数组的作用:是将指引你下一次探深对比的位置,
搞懂next数组的第一步,必须先要知道 前缀,后缀这两个概念
示例一:
在"abc"中,
前缀就是"ab",除去最后一个字符"c"的剩余字符串。
后缀就是"bc",除去第一个字符"a"后的面全部的字符串。
示例二:
如果子串是"abcabcdabc"
第一次拆分,前缀: abcabcdab 后缀: bcabcdabc,发现前后缀并不是一样的,那么我们需要按照前面拆分前后缀的规则继续拆分
p: abcabcda , s:cabcdabc
p: abcabcd , s:abcdabc
p: abcabc , s:bcdabc
p: abcab , s:cdabc
p: abca , s:dabc
p: abc , s:abc 成功匹配出前后缀中,最长相同子串
这里有一点要注意,前缀必须要从头开始算,后缀要从最后一个数开始算,中间截一段相同字符串是不行的。
next数组推导
根据前后缀的拆分的规则,算出当前下标k,到0,最长的重复子串长度
示例:
A B A B A C A
k = 0 及 next[k] = 0 ,因为首字符长度唯一,无法拆分出前后缀,所以= 0
k = 1 及 next[k] = 0 ,AB ,前后缀无相同所以 = 0
k= 2 及 next[k] = 1,ABA ,前缀AB ,后缀BA,继续-> (F: A ,S:A),所以 = 1
k = 3及 next[k] = 2 ,ABAB ,前缀ABA 后缀BAB ,继续-> (F: AB ,S:AB) 所以 = 2
k = 4 及 next[k] = 3 ,ABABA (F: ABAB , S: BABA) -> (F: ABA , S: ABA) = 3
k = 5 及 next[k] = 0 ,ABABAC (F: ABABA , S: BABAC) -> (F: ABAB , S: ABAC)
-> (F: ABA , S: BAC)-> (F: AB , S: AC)-> (F: A , S: C) = 0
k = 6 及 next[k] = 1 ,ABABACA (F: ABABAC , S: BABACA) -> (F: ABABA , S: ABACA) ->(F: ABAB , S: BACA)->(F: ABA , S: ACA)->(F: AB , S: CA)->(F: A , S: A) = 1
最终得出字符串对应的next[]数组:
翻译成java代码:
private int[] getNextArray(String chs) {
int i;//字符数组的下标指示器
int k;//前一个字符处的最大公共(相等)前、后缀子串的长度
int[] next = new int[chs.length()];
for (i = 1, k = 0; i < chs.length(); i++) {
while (k > 0 && chs.charAt(i) != chs.charAt(k)) {
k = next[k - 1]; // 这一部分是在回溯 “前缀” 的位置???
}
if (chs.charAt(i) == chs.charAt(k)) {
k++;
}
next[i] = k;
}
return next;
}next数组的实际应用:
当我们求出一段子串的next数组后,那么在匹配主串的时候,我们可以通过下标,使得我们知道将子串定位到主串 (下一趟探深比较的位置),而没有回退过主串指针。我们希望这个相等的前缀和后缀的长度越大越好(这也是为什么我们next数组中是求相等的前缀和后缀中最长的那个相等串的值),显然这样就可以匹配更多相同的元素。
根据next对应的(下标)推算下一次右移的位数:
如果子串当前匹配失败的位置 为起始点 k = 0 ,默认右移一位,
否则
下一次需要移动的位数 = (已经匹配成功的个数 - next[k-1])
KMP示例:
主串 S: deabcabcdabcabdh
子串 T: abcabd
推算出子串的next数组值 如下:
1第一次对比:
根据next数组计算下一次探深对比的位置 :
失败的位置 为子串起始点 k = 0 ,默认右移一位, moveRight = 1
第二次对比:
失败的位置 为子串起始点 k = 0 ,默认右移一位, moveRight = 1
第三次对比:
moveRight = 5 - next[k - 1] , 及 5 - 2 moveRight = 3
第四次对比:
moveRight = 3 - next[k - 1] , 及 3 - 0 moveRight = 3
第五次对比:
失败的位置 为子串起始点 k = 0 ,默认右移一位, moveRight = 1
第六次匹配:
完成匹配。
KMP增强版本 - nextVal数组
在某些特殊的情况下,next是存在着缺陷的
例如 主串为:aaaabcde ,子串为:aaaaax
先按照next数组推导子串
和主串进行匹配
step1:
发现下标5位置匹配失败:根据公式计算子串下一次右移的范围
moveRight = 5 - next[k - 1] , 及 5 - 4 moveRight = 1
step2:
右移后,我们发现这时的主串下标发生回溯 (i=5 变成i=1),这就和朴素模式没有区别了,不划算。
那么我们换个方式,尝试推导nextVal数组
nextVal推导过程
这里的next数组和上面的next数组推导有一些区别,下标是从1开始,这里的next数组计算的前后缀是截取k=1 到 当前k-1的位置。根据next数组推导,还有一种不基于next的方法还没搞懂):
next[ k = 1] :默认为0,没得比,单身狗不解释。
next[ k = 2] : 当前位置的next值为1, 拿当前a 和next值1对应的a相比 相等 结果为0
next[ k = 3] : 当前位置的next值为2, 拿当前a 和next值2对应的a相比 相等 结果为0
next[ k = 4] : 当前位置的next值为3, 拿当前a 和next值3对应的a相比 相等 结果为0
next[ k = 5] : 当前位置的next值为4, 拿当前a 和next值4对应的a相比 相等 结果为0
next[ k = 6] : 当前位置的next值为5, 拿当前x 和next值5对应的a相比 不通 结果为5
小结:在计算出当前位置的next值的时候,如果当前为的a字符与它的值指向的位置字符b相等,那么当前位置a字符的nextVal值就指向b字符位置的next值,如果不等,该位置a字符的nextVal 的值就是该字符a当前的next值。
通过nextVal数组值匹配
第一位匹配失败时,右移默认一位(如果你想说,子串a的后面有相同字符,而且和主串对应的位置刚好匹配,也可以视为多余的比较,但是我想的是,第一位匹配失败后,是没有必要继续当前i++对比的,因为第一位都失败了,再去比较的意义就不大了)
其他位置失败,通过该位置的nextVal值,对应吧该值指向的字符位移到当前失败的位置即可。完美~!
总结:KMP算法,在较少字符串的情况下进行匹配,相比朴素算法是毫无优势可言的,因为next数组推算,也是需要增加计算成本的。