前言
因为最近在刷剑指,看到有大神用自动机,然后就研究了下,进而看了看KMP算法,但是很多资料感觉太啰嗦,看起来着实有点费劲,所以,结合自己的理解,想写一篇简易通俗的KMP。
☆☆☆ 算法目录导航页,包含基础算法、高级算法、机器学习算法等☆☆☆
1.KMP的定义
KMP其实就是一种改进的字符串匹配(查找)算法,为什么叫KMP呢,是因为其发现者Knuth-Morris-Pratt 这3位大神的名字第一个字母是KMP,并没其他特殊含义,所以关于定义请死记硬背吧O(∩_∩)O~。
KMP算法的关键是利用匹配失败后的信息,尽量减少模式串与主串的匹配次数以达到快速匹配的目的。具体实现就是实现一个next()函数,函数本身包含了模式串的局部匹配信息。时间复杂度O(m+n)。
2.算法过程
其实上面的解释对于初学者来说不好理解,那我们举个例子:
假设已有字符串称为a串, BBC ABCDAB ABCDABCDABDE,求是否含有子串称之为b串ABCDABD。
① 开始匹配
我们发现a串前4个字符都跟b匹配不上,所以一步一步来到了第5个字符a[4] = A处,此时匹配成功就一直匹配,直到a[10]处匹配失败:
接下来,我们看下暴力求解和KMP如何进行匹配:
①暴力求解:b串向后移动一步进行匹配,即匹配失败时,b串只能一步一步的往后移动。时间复杂度为O(m * n)。
② KMP求解:
③ 分析:暴力求解是不是傻,正常人眼一看,匹配失败时,肯定是要跳到a[8]处再进行匹配的,这样效率肯定高啊。所以,KMP其实就是正常人的一种做法,前面的信息已经比较过了,是有记忆依赖的,而不是相互独立的。难道正常人前一秒看见的东西下一秒就会忘了?那是真的傻。这个地方的思路跟动态规划和lstm等算法还是有异曲同工之妙的。
④ 总结:所以,KMP就是让我们的算法有记忆功能,提高算法效率。这种类型算法的实现无非就是空间换时间。所以,我们需要建立一张记忆表,用于查询之前的记忆内容,KMP中叫next数组,它是由b串得来,存储的是b串的前缀后缀中最长的公共字串的长度再整体往后移动一位的值,下面会举例说明。
3.b串移动公式
公式就意味着是一种规律,前人总结,后人即用即可。
只要移动b串,说明已经失配。
移动公式:移动位数 = 已匹配字符数 - 失配字符的上一位字符所对应的最大长度值
4.最大长度表求解过程
最大长度指的是前缀后缀中最长公共字串的长度。举例说明:
5.b串移动公式 换种形式
b串位移公式变为:移动位数 = 失配字符所在位置 - 失配字符对应的next 值
6.next表求解过程
为了编程好实现,又定义了next表,其实就是最大长度表整体右移一位:
Java实现 next表
/**
*
* @param b_str 需要查找或匹配的字符串
* @param next 由最长的相同前缀后缀的子串长度表 而 构建的next表
*
* 0 0 0 0 1 2 0 前缀后缀最长子串长度表
* -1, 0, 0, 0, 0, 1, 2 getNext得到(前面长度表右移1)
*
*/
static void getNext(char [] b_str,int [] next)
{
int bLen = b_str.length;
next[0] = -1;//因为最大长度表右移一位后,第一位补位 -1
int i = -1;
int j = 0; //b串起始索引
while (j < bLen - 1)//遍历b串
{
//b_str[i]表示前缀的第一个字符,b_str[j]表示后缀的第一个字符
if (i == -1 || b_str[i] == b_str[j])
{
i++;
j++;
next[j] = i;//当前比较字符,即后缀的第一个字符更新其next值
}
else
{
i = next[i];
}
}
}
7.根据移动公式求b串是否为a的子串
Java 完整实现
/**
* kmp
*
* 字符串匹配查找算法
*
* @author wxq
*
*/
public class KMP {
public static void main(String[] args) {
String a_str = "BBC ABCDAB ABCDABDE";
String b_str = "ABCDABD";
int [] next = new int [b_str.length()];
getNext(b_str.toCharArray(),next);
System.out.println(KmpSearch(a_str.toCharArray(),b_str.toCharArray(), next));
// System.out.println(Arrays.toString(next));
}
/**
* kmp搜索匹配字符串
* @param a_str
* @param b_str
* @param next
* @return
*/
static int KmpSearch(char [] a_str, char [] b_str,int [] next)
{
int i = 0;
int j = 0;
int aLen = a_str.length;
int bLen = b_str.length;
while (i < aLen && j < bLen)
{
//①如果j = -1,或者当前字符匹配成功(即S[i] == P[j]),都令i++,j++
if (j == -1 || a_str[i] == b_str[j])
{
i++;
j++;
}
else
{
//②如果j != -1,且当前字符匹配失败(即S[i] != P[j]),则令 i 不变,j = next[j]
//next[j]即为j所对应的next值
j = next[j];
}
}
if (j == bLen)
return i - j;
else
return -1;
}
/**
*
* @param b_str 需要查找或匹配的字符串
* @param next 由最长的相同前缀后缀的子串长度表 而 构建的next表
*
* 0 0 0 0 1 2 0 前缀后缀最长子串长度表
* -1, 0, 0, 0, 0, 1, 2 getNext得到(前面长度表右移1)
*
*/
static void getNext(char [] b_str,int [] next)
{
int bLen = b_str.length;
next[0] = -1;//因为最大长度表右移一位后,第一位补位 -1
int i = -1;
int j = 0; //b串起始索引
while (j < bLen - 1)//遍历b串
{
//b_str[i]表示前缀的第一个字符,b_str[j]表示后缀的第一个字符
if (i == -1 || b_str[i] == b_str[j])
{
i++;
j++;
next[j] = i;//当前比较字符,即后缀的第一个字符更新其next值
}
else
{
i = next[i];
}
}
}