先后断断续续搞了5个月,每次都觉得本身懂了, 可是要写的时候都不知从何下手,而后又是各类找博客,看帖子,因此此次试着用本身的语言写一个博客。
首先,KMP算法就是从一个模板字符串(S) 中匹配目标字符串(P)。匹配的话,首先就是想到了暴力匹配,也就是用两个下标表示在S的下标(si) 和 P的下标(pi), 而后进行循环,若是s.chatAt(si)==p.chatAt(pi)
就是si ++, pi++;
若是不相等的话,就须要把si = si - pi + 1, pi = 0;
,而后判断 pi == p.length()
相等的话,就是匹配成功,能够返回, 不相等就继续。 下面贴一下代码, 图就不画了。算法
public int violenceMatch(String s, String p){ int sLen = s.length(), pLen = p.length(); int si = 0, pi = 0; while (si < sLen && pi < pLen) { if (s.charAt(si) == p.charAt(pi)) { si++; pi++; } else { si = si - pi + 1; pi = 0; } } if (pi == pLen) { return si - pi; } else { return -1; } }
使用暴力匹配的缺点很明显,就是每次失配(就是s.chatAt(si) != p.chatAt(pi)
的时候,须要把 si 的位置 置为s.chatAt(si)==p.chatAt(pi)
开始的点的下一位,这样会出现不少重复无效的匹配。
KMP算法就是把这些重复无效的匹配解决了,具体怎么解决,这个也是KMP算法的精髓(next数组的求解)。 关于next数组的求解,咱们稍后说,咱们先体会一下 怎么使用KMP算法来进行字符串匹配(若是只想了解next数组是怎么求出来的,能够跳过这部分), 举个例子,有模式串S: "CDABADABCABADABAB"
, 目标串P: "ABADABAB"
用目标串推出的next数组是{0,0,1,0,1,2,3,2}(后面会具体讲怎么推出来的),如今咱们开始使用KMP算法进行匹配。一开始是 si = 0, pi = 0
。
咱们能够看到这个位置不匹配的,而后由于当前pi == 0 因此直接si += 1 pi 不动 进行下一步 此时 si = 1,pi = 0
数组
此时也是不匹配的, 而后重复上一步, 此时 si = 2, pi = 0
3d
当si = 2, pi= 0
的时候,s.chatAt(si) == p.chatAt(pi)
,因此此时 si += 1, pi += 1
, 重复这样匹配,咱们发如今si = 8,pi=6
的时候失配了,这是用就须要用到咱们的next数组了。
这里先简单说next数组的同样,是当前下标所对应的最长公共先后缀,注意是最长,不是个数,是长度!!! 公共先后缀,都是基于当前下标来讲的。举个例子 ABA 这个next数组是 {0 0 1}
对于0下标,没有先后缀, 由于只有1个数, 对于1的下标,前缀是A, 后缀是B,A != B, 因此仍是0, 对于2的下标,前缀有 A, AB,后缀有 BA,A,因此值为1 ,后面会有详细的介绍,这里只要分辨出前缀和后缀就能够了。
回到正题, 咱们当前位置是失配, 因此须要用到next数组,那么这个next数组在这里有什么用呢? 咱们试想一下,在当前下标失配, 说明我前面的都是能够匹配上的,咱们的next数组是保存了最长的公共先后缀,咱们是否是能够把失配下标的前一个位置在next数组中对应的最大公共先后缀值来做为目标串(P)移动的距离,由于我当前失配的下标的前一个下标有必定的匹配距离,而后这个下标所对应的前缀是否是能够省略比对,直接移动最长公共先后缀的距离。 这里pi = 6的时候失配, next[6 - 1] = 2, 也就是前缀AB (下标0、1)和 后缀AB(下标四、5),咱们是否是能够省略AB的比较,直接从 ABAD的A开始继续匹配。由于对于 pi = 6来讲, pi = 4, pi=5都是和S串上能够匹配上,省略pi = 0, pi = 1的比较,直接从pi = 2开始和si= 8 继续比对,因此下标变化是si = 8, pi = next[6 - 1]=2
也就是下图:
code
此时对于si = 8, pi= 2
仍然没有匹配上,而后再次使用next数组, next[2 - 1] = 0,因此有 si = 8, pi = next[2 - 1] = 0
blog
此时仍是没有匹配上,可是pi = 0, 因此 si+=1,此时 si = 9, pi = 0
字符串
后面下去都是匹配上了。因此能够返回下标。
可能看到这里,你仍是疑惑这个next下标为何要这样用呢?这里总结一下,而后就解释next数组的推导过程。 咱们在 失配的时候,就须要移动目标串,问题是移动多少呢?不一样于暴力匹配的作法,将 si和pi都一块儿移动,而是只移动 pi,这个移动的距离,和next数组有关,咱们当前失配的位置的前一个位置是能够和S模式串失配前的位置是能够匹配的,因此咱们只要移动当前pi的前一个位置的最大公共先后缀距离,而后本来由后缀匹配的字符给前缀匹配(由于知道了最大公共先后缀的距离,因此这部分只是移动而已,不须要再从新的匹配),而后在失配的地方继续进行新的比对。
这里开始讲解一下next的推导。咱们在前面提到过,next数组对于当前下标所对应的最长公共先后缀,因此咱们从index = 1 开始,由于 0 下标只有1个字符,没有先后缀
get
对于下标1,咱们能够很清楚的看到, 前缀是A, 后缀是B,A != B, 因此next[index] = 0,对下标index = 2进行查看
博客
对于下标2,咱们也能够很清楚的看到,前缀是A、AB,后缀是BA,A,只有A == A,因此next[index] = 1,好像到这里仍是很简单,咱们能够先推出一个公式,p.chatAt(index) == p.chatAt(next[index - 1])
成立的话 next[index] = next[index - 1] + 1
, 不成立的话 next[index] = 0
,后面咱们就用这个公式进行求解,看下这个公式是否成立,在验证结果以前,我先说一下为何会得出这样的公式, next数组是保存了最长公共先后缀,这个概念说过不少次了,由于它特别重要。 咱们对于当前下标,要想找到最长的公共先后缀,最好的办法就是在前一个下标的最长公共先后缀的基础上+1,这点没有问题吧,因此就有了 p.chatAt(index) == p.chatAt(next[index - 1])
。 那么接下来,咱们就来验证一下这个公式的正确性了。对于下标 index = 3,
io
有p.chatAt(3) != p.chatAt(next[3 - 1])
因此next[3] = 0,咱们也能够看出next[3]确实是0, 继续 index = 4
模板
在index = 4的时候,有 p.chatAt(4) == p.chatAt(next[4 - 1])
因此next[4] = next[4 - 1] + 1,确实没错,继续index = 5
在next = 5的时候,有p.chatAt(5) == p.chatAt(next[5 - 1])
因此next[5] = next[5 - 1] + 1,也没有错误 ,继续 index = 6
在next =6 的时候, 有p.chatAt(6) == p.chatAt(next[6 - 1])
, 因此next[6] = next[6 - 1] + 1, 也没有错误,继续 index = 7
在next = 7 的时候, 有p.chatAt(7) != p.chatAt(next[7 - 1])
, 按照公式,此时的next[7] 应该是0 才对呀,可是我写的是 2,咱们能够看一下,确实也是2 由于前缀 AB 和后缀AB相等,因此是2, 可是这是为何呢?咱们能够知道 p.chatAt(7) 确实是不等于 p.chatAt(next[7 - 1]),可是不要忘记,咱们的next保存的是最长公共先后缀,next[7 - 1] = 3,说明下标0 、 一、 2和下标四、五、6是一一对应的,因此咱们对下标4 和7进行比较,发现不相等,按照一开始的思路,咱们会把next[7]设为0, 可是咱们能够看一下下标 0、 一、 2 、 3这里,对于下标3 是咱们下标7要比较的,可是看一下下标2的位置在next数组是1,这代表了,对于下标2,的最长公共先后缀是1,在求next[3]的时候,咱们用p.chatAt(3)
和p.chatAt(next[3 - 1])
进行比较,对于如今的下标7, 咱们是否是能够把它当成是下标3 呢? 彻底能够,由于下标0、一、2和下标四、五、6一一对应, 下标3 和7 没有匹配上,就能够把下标7 当作是下标3, 此时应该是用 p.chatAt(7) 和 p.chatAt(next[3 - 1])
, 对于为何前面是7 后面是next[3- 1] 而不是next[7 - 1]的,若是用next[7 - 1]了, 是否是就陷入了死循环了? 其实这里也就是把3的下标看成是7来看待,对于3前面的没有其余影响,因此才是这样的。 那么到了此时,咱们能够很清晰的求出next数组,而后结合前面的讲解, 就是一个完整的KMP了。
第一次写博客写了2000+字,花费了一些心血画图,试图用最简单的话来叙述这个算法,可是好像没有作到,有一些东西在我这个层次尚未看到,因此也没有用到最简单的话来叙述彻底部,你们能多看几遍,也是能够理解这个算法的精妙之处。最后贴一下完整代码:
package com.hl.solution; /** * @author Hl * @create 2021/3/3 0:18 */ public class KMP { public static void main(String[] args) { KMP kmp = new KMP(); String s = "BBC ABCDAB ABCDABCDABDE"; String p = "12"; int i = kmp.kmpMatch(s, p); int j = kmp.violenceMatch(s, p); System.out.println("KMP算法结果: "+i); System.out.println("暴力匹配结果: " + j); } // KMP匹配 public int kmpMatch(String s, String p){ int[] next = getNext(p); int sLen = s.length(), pLen = p.length(); int sl = 0, pl = 0; while (sl < sLen) { if (s.charAt(sl) == p.charAt(pl)) { sl++; pl++; } else if (pl == 0) sl++; else pl = next[pl - 1]; if (pl == pLen) { return sl - pl; } } return -1; } // 求next数组 public int[] getNext(String p){ int[] next = new int[p.length()]; for (int i = 1; i < p.length(); i++) { int index = next[i - 1]; while (index > 0 && p.charAt(i) != p.charAt(index)) { index = next[index - 1]; } if (p.charAt(i) == p.charAt(index)) { next[i] = index + 1; } } return next; } // 暴力匹配 public int violenceMatch(String s, String p){ int sLen = s.length(), pLen = p.length(); int si = 0, pi = 0; while (si < sLen && pi < pLen) { if (s.charAt(si) == p.charAt(pi)) { si++; pi++; } else { si = si - pi + 1; pi = 0; } } if (pi == pLen) { return si - pi; } else { return -1; } } }
但愿你们都能在我这里获得一些收获,感谢看了这么久........