KMP算法详解
介绍
KMP算法是一种改进的字符串匹配算法,其关键是利用匹配失败后的信息,尽量减少模式串与主串的匹配次数以达到快速匹配的目的。
在暴力匹配中,每趟匹配失败都是模式后移一位再从头开始比较。而某趟已匹配相等的字符序列是模式的某个前缀,这种频繁的重复比较相当于模式串在不断地进行自我比较,这就是其低效率的根源。
因此,可以从分析模式本身的结构着手,如果已匹配相等的前缀序列中有某个后缀正好是模式的前缀,那么就可以将模式向后滑动到与这些相等字符对齐的位置,主串指针无须回溯,并继续从该位置开始进行比较。而模式向后滑动位数的计算仅与模式本身的结构有关,与主串无关。
- 字符串的前缀:指除最后一个 字符以外,字符串的所有头部子串;
- 字符串的后缀:指除第一个字符外,字符串的所有尾部子串;
- 部分匹配值:字符串的前缀和后缀的最长相等前后缀长度。
例:‘ababa’
- 'a’的前缀和后缀都为空集,最长相等前后缀长度为0 。
- 'ab’的前缀为{a},后缀为{b},最长相等前后缀长度为0 。
- ……
- 'ababa’的前缀:{a,ab,aba,abab},后缀:{a,ba,aba,baba},最长相等前后缀长度为3。
因此’ababa’的部分匹配值为00123。
Brute-Force 暴力匹配代码
/**
*@MethodName bruteForce
*@Description TODO
*@Date 2022-07-16 20:01
*@Param [str1 主串, str2 模式串]
*@ReturnType int
*/
public static int bruteForce(String str1,String str2){
//i主串指针,j模式串指针
int i = 0,j=0;
while (i < str1.length()&&j < str2.length()) {
if (str1.charAt(i)==str2.charAt(j)){
j++;
i++;
}else {//不匹配
//i回退到本次匹配开始的字符 位置 的后一个字符位置
i=i-j+1;
//j会退到0
j=0;
}
}
if (j>=str2.length()){//说明模式串全部成功匹配了
return i-str2.length();
}
return -1;
}
KMP基本原理
KMP算法就是在匹配字符串时,利用一个next数组实现,不回退主串指针i 的情况下,确定j回退的位置,来快速匹配。(next数组也就是部分匹配值。next数组的代码实现思路在本章后半部分,这部分的next数组采取人力计算方法。)
上面提到了,如果已匹配相等的前缀序列中有某个后缀正好是模式的前缀,那么就可以将模式向后滑动到与这些相等字符对齐的位置,主串指针无须回溯,并继续从该位置开始进行比较。
其中,**j回退位数=已匹配字符数-对应部分匹配值 **
PS:next数组有两种形式,一种是现在说的部分匹配值,next[0]=0开始的;另一种是将部分匹配值整体右移一个位置,最右侧补-1,即以next[0]=-1开始的。两者本质是一样的。
下面以主串·'ababcabcacbac’模式串’abcac’来展示以下KMP匹配过程:
序号 | 0 | 1 | 2 | 3 | 4 |
---|---|---|---|---|---|
模式串 | a | b | c | a | c |
next | 0 | 0 | 0 | 1 | 0 |
第一次匹配:
第二次匹配:j回退 已匹配字符数-对应部分匹配值 =2-next[1]=2 位
第三次匹配:j回退 已匹配字符数-对应部分匹配值 =4-next[3]=4 位
代码实现:
(主要部分,next数组部分在后面)
public static int kmpsearch(String str1, String str2, int[] next) {
/**
*@MethodName kmpsearch
*@Param [str1 源字符串, str2 子串, next 部分匹配位置]
*@ReturnType int如果是-1没有匹配到,否则返回第一个匹配的位置
*/
for (int i = 0, j = 0; i < str1.length(); i++) {
//匹配失败后回退j
while (j > 0 && str1.charAt(i) != str2.charAt(j)) {
//j=j- (已匹配字符数-对应部分匹配值)= j-(j-next[j-1])=next[j-1]
j = next[j - 1];
}
if (str1.charAt(i) == str2.charAt(j)) {
j++;
}
//匹配完成后返回匹配字符串位置(第一个字符位置)
if (j == str2.length()) {
return i - j + 1;
}
}
return -1;
}
next数组怎么求呢?
首先,明确 n e x t [ i ] next[i] next[i]的含义:前缀后缀的最大相同字符长度。
从左边开始扫描模式串,则扫描到第i-1个时next[i-1]的状态如下图所示:
那么可以计算next[i]:
- 如果模式串dest[i]==dest[ next[i-1] ] (即dest[n]),那么很明显next[i]=next[i-1]+1 =n+1
- 如果不相等,那么我们要对dest[n-1]的字符有所期待,万一 第0~n-1个字符和后缀相同呢?如下:
对于前0~n-1的字符同样存在m长度的相同前后缀,即next[n-1]=m。
- 那么如果dest[i]==dest[n-1] 也就是dest[i]==dest[ next[n-1] ],那么很明显next[i]=next[n-1]+1 =m+1。
- 如果不相等,那么我们继续对dest[m-1]的字符有所期待。
这和上面的步骤是不是一样的?这样就可以依次循环下去直至找到相同字符,或者直至前面没有字符为止。
代码实现:
上述过程转换成代码:
/**
*@MethodName kmpnext
*@Description TODO 获取一个字符串(子串)的部分匹配值
*@Date 2022-07-16 22:28
*@Param [dest]
*@ReturnType int[]
*/
public static int[] kmpnext(String dest) {
int[] next = new int[dest.length()];
next[0] = 0;
for (int i = 1, j = 0; i < dest.length(); i++) {
//当dest.charAt(i) != dest.charAt(j),我们需要从next[j-1]获取新的j
//直到我们发现有dest.charAt(i) == dest.charAt(j)成立才退出
while (j > 0 && dest.charAt(i) != dest.charAt(j)) {
j = next[j - 1];
}
//当满足dest.charAt(i) == dest.charAt(j)时,部分匹配值+1
if (dest.charAt(i) == dest.charAt(j)) {
j++;
}
next[i] = j;
}
return next;
}
整体代码
public static int kmpsearch(String str1, String str2, int[] next) {
/**
*@MethodName kmpsearch
*@Param [str1 源字符串, str2 子串, next 部分匹配位置]
*@ReturnType int如果是-1没有匹配到,否则返回第一个匹配的位置
*/
for (int i = 0, j = 0; i < str1.length(); i++) {
//匹配失败后回退j
while (j > 0 && str1.charAt(i) != str2.charAt(j)) {
//j=j- (已匹配字符数-对应部分匹配值)= j-(j-next[j-1])=next[j-1]
j = next[j - 1];
}
if (str1.charAt(i) == str2.charAt(j)) {
j++;
}
//匹配完成后返回匹配字符串位置(第一个字符位置)
if (j == str2.length()) {
return i - j + 1;
}
}
return -1;
}
/**
*@MethodName kmpnext
*@Description TODO 获取一个字符串(子串)的部分匹配值
*@Date 2022-07-16 22:28
*@Param [dest]
*@ReturnType int[]
*/
public static int[] kmpnext(String dest) {
int[] next = new int[dest.length()];
next[0] = 0;
for (int i = 1, j = 0; i < dest.length(); i++) {
//当dest.charAt(i) != dest.charAt(j),我们需要从next[j-1]获取新的j
//直到我们发现有dest.charAt(i) == dest.charAt(j)成立才退出
while (j > 0 && dest.charAt(i) != dest.charAt(j)) {
j = next[j - 1];
}
//当满足dest.charAt(i) == dest.charAt(j)时,部分匹配值+1
if (dest.charAt(i) == dest.charAt(j)) {
j++;
}
next[i] = j;
}
return next;
}