KMP算法
文章目录
- KMP算法
- KMP算法介绍
- 主要逻辑
- Next数组
- KMP搜索
- 代码解释
- 生成next数组
- 模式串匹配
- 源码展示
KMP算法介绍
KMP算法是一种串的模式匹配算法,用来求子串在主串的位置。是数据结构中比较难的一种算法。KMP算法的核心在于点在于如何利用子串生成next数组,和如何利用next数组来寻找子串在主串的位置
主要逻辑
在模式串匹配的过程中,一般的思路是利用两个指针,分别指向主串和模式串(子串),然后从头比较,如果两个指针指向的字符是一样的,就将两个指针同时后移,接着比较后面的字符。当两个指针指向的字符不再一样的时候,就要将指向主串元素的指针回溯到一定位置,同时指向模式串元素的指针也回溯到一定位置,然后重新比较。
当指向模式串元素的指针遍历完模式串的同时,恰好遍历完主串或者没有遍历完主串,则说明已经找到模式串在主串中的位置
当指向模式串元素的指针没有遍历完模式串的时候已经把主串遍历完了,则说明模式串在主串中不存在匹配的位置
这种思路最主要的地方在于如何让模式串右移的过程做到高效,此时就有了两种思路,一种是简单的BF算法(暴力算法)一种就是KMP算法
BF算法主要是最小程度的将模式串右移,从而达到不遗漏任何一个可能的目的。在模式串匹配的工程中,当主串上的一个字符和模式串不匹配的时候,就将指向主串元素的指针回溯到主串起始比较的下一个元素,指向模式串元素的指针回溯到模式串最开始元素,然后继续开始比较。这种思路主要每次比较失败都将模式串右移一个字符的长度,时间复杂度是O(n*m)
KMP算法相比于BF算法主要的特点是利用next数组提高模式串比较的效率。在模式串比较的过程中,根据已经生成next数组将将模式串最大限度的右移。在将模式串右移的过程中,还做到了指向主串元素的指针不回溯,按照next数组将指向模式串数组的指针进行回溯。时间复杂度为O(n+m)
Next数组
当我们在进行字符串匹配的BF算法过程中,因为在一遍一遍的遍历模式串才会导致时间复杂度那么高。当我们如果可以遍历完一遍模式串以后将模式串的规律存储到一个数组中,然后根据这个数组中存储的数据进行回溯的话,就会大大降低时间复杂度,这就是为什么要生成next数组的原因。
一个人能能走的多远不在于他在顺境时能走的多快,而在于他在逆境时多久能找到曾经的自己。 ——KMP
Next数组是KMP算法的核心,Next数组决定了如何将模式串做到最大程度的右移,在next数组中存储的数据代表着如果当前字符模式串匹配失败,指向模式串元素的指针要回溯的位置。这个回溯的位置一般是看模式串中相同的字符串前缀和字符串后缀,当一个字符串前缀和后缀一致的时候,就会进行按照字符串前缀和字符串后缀的长度进行生成next数组的数值。
在一个字符串中,它有字符串前缀和字符串后缀,前缀是指最后一个字符以外,一个字符串的全部头部的组合,后缀是指第一个字符以外,一个字符串的全部尾部组合。就以字符串"ababc",它的字符串前缀有a,ab,aba,abab,它的字符串后缀有c,bc,abc,babc,把每一个字符串前缀都当成一个独立的字符串,寻找每个字符串的公共前后缀。‘a’,‘ab’没有公共前后缀,"aba"存在公共前后缀,是’a’,所以生成的模式匹配值是1,“abab"存在公共前后串,是"ab”,所以生成的模式匹配值是2。最后字符串"ababac"生成的next数组是00120,生成好Next数组就可以根据这个数组进行KMP模式串匹配
KMP搜索
KMP搜索核心点之一就是如何利用next数组,已知next数组是根据模式串的规律进行生成的,如何利用这个规律将模式串匹配做到更快更高效就是KMP算法的一个关键。
根据Next数组的介绍可以得知,next的数值代表着最长模式串前缀子串的公共前后缀的最长长度,同时也代表着要回溯的位置。当模式串和主串的元素不再一致的时候,就要根据next数据将指向模式串字符的指针进行回溯,回溯的位置就是next数组的值。当next数组此时的值为0的时候,就要根据情况进行一遍检查。
代码解释
生成next数组
//获取到一个字符串的部分匹配值
public static int[] KMP_next(String dest) {
//创建一个数组next,保存部分匹配值。长度跟模式串长度一致
int[] next = new int[dest.length()];
next[0] = 0;//如果字符串是长度为1 部分匹配值就是0
//当字符串的长度为1的时候,没有前缀也没有后缀,更不可能有公共前后缀,所以默认是0
//i从第二位开始,j从第一位开始,遍历整个模式串
for (int i = 1, j = 0; i < dest.length(); i++) {
//当dest.charAt(j) != dest.charAt(i),我们需要从next[j-1]获取新的j
//知道我们发现有dest.charAt(j) == dest.charAt(i)成立才停止
while (j > 0 && dest.charAt(j) != dest.charAt(i)) {
//这里设置j>0是为了防止陷入死循环中(i指向的元素不等于第一个元素,并且j)
j = next[j - 1];//当est.charAt(j) != dest.charAt(i),进行一个回溯,一直到dest.charAt(j) == dest.charAt(i)或者j已经指向第一个元素的位置
}
//当dest.charAt(j) == dest.charAt(i)满足时,部分匹配值就是+1
if (dest.charAt(j) == dest.charAt(i)) {
j++;//这里进行j++,刚好让j进一位,但是还是在i后面
}
next[i] = j;//将j的位置放进next数组中
}
return next;
}
模式串匹配
//KMP搜索算法
public static int KmpSearch(String str1, String str2) {
//调用KMP_next方法生成一个next数组
int[] next = KMP_next(str2);
//遍历主串
for (int i = 0, j = 0; i < str1.length(); i++) {
//这里也是一个回溯,但是这里的回溯是根据next数组,将指向模式串元素的指针回溯到指定位置,从而实现模式串的右移。
if (j > 0 && str1.charAt(i) != str2.charAt(j)){
j = next[j - 1];
}
//如果模式串和主串的被指向的元素相同就将j后移,循环结束后i会自动后移一位,所以这里就不用写i++
if (str1.charAt(i) == str2.charAt(j)) {
j++;
}
//判断j是不是已经指向模式串最后一个字符,如果指向了模式串最后一个字符,就说明已经在主串中找到跟模式串相同的子串,此时直接根据i,j就可以确定子串在主串中位置
if (j == str2.length()) {
return i - j + 1;
}
}
//当主串遍历完成但是没有提前return的时候,说明没有模式串匹配失败,没有在主串中找到和模式串相同的子串,此时就要返回一个主串中不存在的位置用来说明没有找到,一般用-1来说明
return -1;
}
for循环里面的三个if判断不能调换顺序,判断j是否指向模式串最后一个元素的位置的if判断必须放到后面,因为放到开头时会发生i已经自增一次的情况,return语句也要相应的发生变化。
源码展示
import java.util.Arrays;
public class KMP {
public static void main(String[] args) {
String str1 = "BBC ABCDAB ABCDABCDABDE";
String str2 = "ABCDABA";
int[] next = KMP_next(str2);
System.out.println("next=" + Arrays.toString(next));
int index=KmpSearch(str1,str2);
System.out.println(index);
}
//KMP搜索算法
public static int KmpSearch(String str1, String str2) {
int[] next = KMP_next(str2);
//遍历
for (int i = 0, j = 0; i < str1.length(); i++) {
if (j>0&&str1.charAt(i)!=str2.charAt(j)){
j=next[j-1];
}
if (str1.charAt(i) == str2.charAt(j)) {
j++;
}
if (j == str2.length()) {
return i - j + 1;
}
}
return -1;
}
//获取到一个字符串的部分匹配值
public static int[] KMP_next(String dest) {
//创建一个数组next,保存部分匹配值
int[] next = new int[dest.length()];
next[0] = 0;//如果字符串是长度为1 部分匹配值就是0
for (int i = 1, j = 0; i < dest.length(); i++) {
//当dest.charAt(j) != dest.charAt(i),我们需要从next[j-1]获取新的j
//知道我们发现有dest.charAt(j) == dest.charAt(i)成立才停止
while (j > 0 && dest.charAt(j) != dest.charAt(i)) {
j = next[j - 1];
}
//当dest.charAt(j) == dest.charAt(i)满足时,部分匹配值就是+1
if (dest.charAt(j) == dest.charAt(i)) {
j++;
}
next[i] = j;
}
return next;
}
}