解读KMP算法
前后断断续续搞了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", 目标串 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
当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(下标4、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也就是下图:
此时对于si = 8, pi= 2 仍然没有匹配上,然后再次使用next数组, next[2 - 1] = 0,所以有 si = 8, pi = next[2 - 1] = 0
此时还是没有匹配上,但是pi = 0, 所以 si+=1,此时 si = 9, pi = 0
后面下去都是匹配上了。所以可以返回下标。
可能看到这里,你还是疑惑这个next下标为什么要这样用呢?这里总结一下,然后就解释next数组的推导过程。 我们在 失配的时候,就需要移动目标串,问题是移动多少呢?不同于暴力匹配的做法,将 si和pi都一起移动,而是只移动 pi,这个移动的距离,和next数组有关,我们当前失配的位置的前一个位置是可以和S模式串失配前的位置是可以匹配的,所以我们只要移动当前pi的前一个位置的最大公共前后缀距离,然后原本由后缀匹配的字符给前缀匹配(因为知道了最大公共前后缀的距离,所以这部分只是移动而已,不需要再重新的匹配),然后在失配的地方继续进行新的比对。
这里开始讲解一下next的推导。我们在前面提到过,next数组对于当前下标所对应的最长公共前后缀,所以我们从index = 1 开始,因为 0 下标只有1个字符,没有前后缀
对于下标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,
有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 、 1、 2和下标4、5、6是一一对应的,所以我们对下标4 和7进行比较,发现不相等,按照一开始的思路,我们会把next[7]设为0, 但是我们可以看一下下标 0、 1、 2 、 3这里,对于下标3 是我们下标7要比较的,但是看一下下标2的位置在next数组是1,这表明了,对于下标2,的最长公共前后缀是1,在求next[3]的时候,我们用p.chatAt(3) 和p.chatAt(next[3 - 1])进行比较,对于现在的下标7, 我们是不是可以把它当成是下标3 呢? 完全可以,因为下标0、1、2和下标4、5、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;
}
}
}
希望大家都能在我这里得到一些收获,感谢看了这么久........
解读KMP算法的更多相关文章
- hdu3336解读KMP算法的next数组
查看原题 题意大致是:给你一个字符串算这里面全部前缀出现的次数和.比方字符串abab,a出现2次.ab出现2次,aba出现1次.abab出现1次.总计6次. 而且结果太大.要求对1007进行模运算. ...
- 算法进阶面试题01——KMP算法详解、输出含两次原子串的最短串、判断T1是否包含T2子树、Manacher算法详解、使字符串成为最短回文串
1.KMP算法详解与应用 子序列:可以连续可以不连续. 子数组/串:要连续 暴力方法:逐个位置比对. KMP:让前面的,指导后面. 概念建设: d的最长前缀与最长后缀的匹配长度为3.(前缀不能到最后一 ...
- BF算法 + KMP算法
准备: 字符串比大小:比的就是字符串里每个字符的ASCII码的大小.(其实这样的比较没有多大的意义,我们关心的是字符串是否相等,即匹配等) 字符串的存储结构:同线性表(顺序存储+链式存储) 顺序存储结 ...
- 简单有效的kmp算法
以前看过kmp算法,当时接触后总感觉好深奥啊,抱着数据结构的数啃了一中午,最终才大致看懂,后来提起kmp也只剩下“奥,它是做模式匹配的”这点干货.最近有空,翻出来算法导论看看,原来就是这么简单(先不说 ...
- KMP算法
KMP算法是字符串模式匹配当中最经典的算法,原来大二学数据结构的有讲,但是当时只是记住了原理,但不知道代码实现,今天终于是完成了KMP的代码实现.原理KMP的原理其实很简单,给定一个字符串和一个模式串 ...
- 萌新笔记——用KMP算法与Trie字典树实现屏蔽敏感词(UTF-8编码)
前几天写好了字典,又刚好重温了KMP算法,恰逢遇到朋友吐槽最近被和谐的词越来越多了,于是突发奇想,想要自己实现一下敏感词屏蔽. 基本敏感词的屏蔽说起来很简单,只要把字符串中的敏感词替换成"* ...
- KMP算法实现
链接:http://blog.csdn.net/joylnwang/article/details/6778316 KMP算法是一种很经典的字符串匹配算法,链接中的讲解已经是很明确得了,自己按照其讲解 ...
- 数据结构与算法JavaScript (五) 串(经典KMP算法)
KMP算法和BM算法 KMP是前缀匹配和BM后缀匹配的经典算法,看得出来前缀匹配和后缀匹配的区别就仅仅在于比较的顺序不同 前缀匹配是指:模式串和母串的比较从左到右,模式串的移动也是从 左到右 后缀匹配 ...
- 扩展KMP算法
一 问题定义 给定母串S和子串T,定义n为母串S的长度,m为子串T的长度,suffix[i]为第i个字符开始的母串S的后缀子串,extend[i]为suffix[i]与字串T的最长公共前缀长度.求出所 ...
随机推荐
- 最短Hamilton路径(状压dp)
最短Hamilton路径实际上就是状压dp,而且这是一道作为一个初学状压dp的我应该必做的题目 题目描述 给定一张 n(n≤20) 个点的带权无向图,点从 0~n-1 标号,求起点 0 到终点 n-1 ...
- Codeforces 1355 C. Count Triangles
传送门:C - Count Triangles 题意:给你四个数A,B,C,D,求有多少个三边为x,y,z (A ≤ x ≤ B ≤ y ≤ C ≤ z ≤ D)的三角形. 题解:枚举 x=A~B, ...
- AtCoder Beginner Contest 162
比赛链接:https://atcoder.jp/contests/abc162/tasks A - Lucky 7 #include <bits/stdc++.h> using names ...
- NCD 2019 H. Mr. Hamra and his quantum particles
题意:给你n个数,有m次操作,每次使得两个数相连接,询问q次,问某两个数是否连接在一起. 题解:这其实是一道并查集的裸题,这里就不再多说了,写个路径压缩的find函数即可. 代码: #include ...
- Codeforces Round #494 (Div. 3) D. Coins and Queries (贪心,数学)
题意:给你一组全是\(2^d\ (d\ge0)\)的数,询问q次,每次询问一个数,问这个数是否能够由原数组中的数相加得到,如果能,输出最少用多少个数,否则输出\(-1\). 题解:首先贪心得出结论:如 ...
- Power Strings POJ - 2406 后缀数组
Given two strings a and b we define a*b to be their concatenation. For example, if a = "abc&quo ...
- 踏上Revit二次开发之路 3 自己的工具按钮
3 自己的工具按钮 上次的例子只能在"附加模块"→"外部工具"下运行,用作个人作品是没问题,如果打算搞个公司产品的话,估计BOSS是不会满意的.这次我来做一个直 ...
- 一个方便 LeetCode 复习的脚本
这个脚本半年前就在用了,只不过一直没有公开. 这是一个简易的 LeetCode 自动统计程序, 可自动统计最近提交通过的题目, 并以 Markdown 的形式展示相关的数据. 采用 GitHub Ac ...
- NLP论文阅读一:Paper阅读方法
参考:https://pan.baidu.com/s/1MfcmXKopna3aLZHkD3iL3w 一.为什么要读论文? 基础技术:读论文中的related works可以帮助了解该领域的一些主要的 ...
- 洛谷p1198 最大数
#include <iostream> #include <cstdio> #include <algorithm> using namespace std; in ...