背景:最近在学习数据结构,学到“串”这一块时,遇到了KMP算法。但是卡了好长时间不是很理解这个算法。今天突然get到这个算法的点了,故写博客记录一下,防止之后遗忘。可能我理解还有什么不到位的地方,如果大家发现了请多多指教。
为了方便叙述,先设定被搜索串(主串)为M,而搜索串(目标串)为T。
关于串的匹配模式,最简单最常用的也就是BF算法了,它的原理就是每次从主串M中取出一个和T等长的子字符串,如果匹配,就返回串的起始位置,如果不匹配,则重新从M中取一个子字符串再次比较。直到找到匹配的字符串或者是搜索完主串但并未找到目标串,搜索结束。代码如下:
int BF(String M, String T){
int m = M.length();
int n = T.length();
for (int i = 0; i < m - n; i++) {
String temp = M.substring(i, i + n);
if (temp.equals(T)) {
return i;
}
}
return -1;
}
正如其名,BF用暴力的手段去查找字符串是否匹配,虽然这是大多数最容易接受的一种方法,但是效率明显低。
现在举个例子主串为“zxcvzbznmasdf”,目标串为“znm”。如果采用BF算法,则会先从主串中拿出“zxc”和目标串比较,发现不匹配,接着再拿出一个字符串“xcv”去比较,以此类推。
但是如果我们自己去比较的话,先从目标串中拿出“zxc”,发现不匹配。接着我们会怎么做呢?肯定不会再去取“xcv”来和目标串比较,因为我们已经知道“xcv”中的后两个字符一定不会和目标串匹配。(我当时就是卡在这个地方的,具体为什么我们再接着讨论)总之,我们知道之前的BF肯定效率低的,因为中间可能有一些子字符串并不需要比较,因为明显不会匹配。
接着,我们再来介绍KMP算法,这个算法的背景就不介绍了,感兴趣的自行百度。
KMP算法引入一个next数组,该数组记录的是目标串的每个位置之前的子字符串(不包括该位置)的相同前缀和后缀的最大长度。
举个例子,目标串“zxcdzxce”,next[0]表示位置0前的子字符串“”的相同前缀和后缀的最大长度,为0;next[4]表示位置4之前的子字符串“zxcd”的相同前缀和后缀,为0;next[5]表示位置5之前的子字符串“zxcdz”的相同前缀和后缀,即“z”的长度,为1;以此类推,得到所有的next数组元素值。
现在我们在俩考虑前面红字部分的困惑。假设假设主串为“zxcdzxcefdsewa”,目标串为“zxcdzxcr”,第一步,我们先从目标串中取出“zxcdzxce”,发现只有最后一个字母不匹配,其对应的next数组为next[7],该位置最长的相同前缀和后缀为“zxc“,值为3。此时,我们知道目标串中前7个字母和主串中的前7个字母是匹配的,而这7个字母组成的字符串的最大相同前缀和后缀是”zxc“,所以,我们会直接从主串的第二个“zxc”开始比较,因为我们知道,这两个“zxc”之间没有“zxc”,肯定不会匹配目标串的,没有必要采用BF算法,因此提高了效率。(这块大家需要慢慢理解)
接着,我们再来讨论next数组怎么求,这个是整个KMP算法的核心部分。这里先贴出最常用的代码:
public static int[] getNext(String ps)
{
char[] strKey = ps.toCharArray();
int[] next = new int[strKey.length];
// 初始条件
int j = 0;
int k = -1;
next[0] = -1;
// 根据已知的前j位推测第j+1位
while (j < strKey.length - 1)
{
if (k == -1 || strKey[j] == strKey[k])
{
next[++j] = ++k;
}
else
{
k = next[k];
}
}
return next;
}
关于这个代码的具体实现原理,我是参照别人的博客理解的,他的博客中用图像解释了代码,很容易理解,这里贴出链接:https://www.cnblogs.com/tangzhengyue/p/4315393.html
最后,我们再来用代码实现KMP算法:
public int KMP(String M, String T){
int m = M.length();
int n = T.length();
char[] ms = M.toCharArray();
char[] ns = T.toCharArray();
int[] next = getNext(T);
int i = 0, j = 0;
while(i < m && j < n){
if (j == -1 || ms[i] == ns[j]){
i++;
j++;
} else {
j = next[j];
}
}
if (j == n)
return i - j;
else
return -1;//表示不存在
}
之后就可以在编译器中带入具体字符串去实验,理解算法。学习这个算法是先了解算法的原理,接着在仔细考虑具体的代码实现原理。