KMP(Knuth-Morris-Pratt)算法是一种用于字符串搜索的高效算法,典型的应用场景是在一个文本字符串中查找一个模式(或子字符串)的出现位置。它的效率之所以高,是因为它能够在不回溯文本字符串指针的情况下,通过预处理模式字符串来有效地跳过字符。
KMP算法的核心思想
KMP算法的核心在于预处理模式字符串,构建一个部分匹配表(也称为KMP表),该表用于在不匹配时决定文本指针的下一步位置。具体而言,部分匹配表记录了模式字符串的每个位置之前(不包括该位置字符)的最长相等前后缀的长度。利用这个表,算法能够在发生不匹配时,将模式字符串向右移动至适当的位置继续匹配,而无需重新检查之前已匹配的字符。
部分匹配表的计算
假设我们有一个模式字符串"ABCDABD",我们现在计算其部分匹配表(以0开始):
- "A"的前后缀都为空集,最长公共元素长度为0
- "AB"的前后缀为空集,最长公共元素长度为0
- "ABC"的前后缀为空集,最长公共元素长度为0
- "ABCD"的前后缀为空集,最长公共元素长度为0
- "ABCDA"的前缀为{"A", "AB", "ABC", "ABCD"},后缀为{"BCDA", "CDA", "DA", "A"},最长公共元素为"A",长度为1
- "ABCDAB"的前缀为{"A", "AB", "ABC", "ABCD", "ABCDA"},后缀为{"BCDAB", "CDAB", "DAB", "AB", "B"},最长公共元素为"AB",长度为2
- "ABCDABD"的前后缀为空集,最长公共元素长度为0
因此,模式字符串"ABCDABD"的部分匹配表为lps[0, 0, 0, 0, 1, 2, 0]
。
搜索过程
KMP算法利用部分匹配表进行搜索时,当文本字符串中的字符与模式字符串中的字符不匹配时,可以根据模式字符串的部分匹配表值跳过一些字符匹配尝试。这样,算法就不需要从文本字符串的下一个字符重新开始匹配模式字符串,从而提高搜索效率。
以模式字符串 "ABCDABD" 和一个假设的文本字符串 "ABC ABCDAB ABCDABCDABDE" 为例:
匹配过程:
文本 (txt): "ABC ABCDAB ABCDABCDABDE" 模式 (pat): "ABCDABD" 文本索引 (i): 0 模式索引 (j): 0。
第一轮匹配:
txt[0] = 'A', pat[0] = 'A' -> 匹配成功, i 和 j 都加 1
txt[1] = 'B', pat[1] = 'B' -> 匹配成功, i 和 j 都加 1
txt[2] = 'C', pat[2] = 'C' -> 匹配成功, i 和 j 都加 1
txt[3] = ' ', pat[3] = 'D' -> 匹配失败, j 不为 0, 所以 j 跳转到 lps[j-1] = lps[2] = 0
第二轮匹配(从文本索引4开始,因为 i 不回溯):
txt[4] = 'A', pat[0] = 'A' -> 匹配成功, i 和 j 都加 1
以此类推,直到 pat[4] = 'A', txt[8] = 'A' 时匹配成功,j 加 1 到 5
txt[9] = 'B', pat[5] = 'B' -> 匹配成功, j 加 1 到 6
txt[10] = ' ', pat[6] = 'D' -> 匹配失败, j 跳转到 lps[j-1] = lps[5] = 2
第三轮匹配:(文本索引为11,模式索引跳到2):
txt[11] = 'A', pat[2] = 'C' -> 匹配失败, j 跳转到 lps[j-1] = lps[1] = 0
后续匹配:
继续以上步骤,直到发现整个模式字符串在文本字符串中的匹配位置。在这个例子中,完整的匹配将在文本字符串的索引15开始。KMP算法通过部分匹配表(lps)避免了从文本字符串的每一个新位置重新开始搜索模式字符串,这显著提高了搜索效率。当在某一位置发现不匹配时,从部分匹配表中可以得出下一个可能匹配的模式字符串的位置,从而避免了不必要的比较。
算法的优势
KMP算法的主要优势在于其搜索效率。在最坏的情况下,KMP算法的时间复杂度是O(n),其中n是文本字符串的长度。这比最简单的字符串搜索算法(如朴素字符串搜索算法,其时间复杂度为O(n*m),其中m是模式字符串的长度)要高效得多。
例题1:
P3375 【模板】KMP - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
String s1 = sc.next();
String s2 = sc.next();
int[] lps = computeLPSArray(s2);
search(s1, s2, lps);
// 打印每个长度的前缀的最长 border 长度
for (int i = 0; i < lps.length; i++) {
System.out.print(lps[i] + " ");
}
}
// KMP搜索函数
public static void search(String txt, String pat, int[] lps) {
int M = pat.length();
int N = txt.length();
int i = 0; // txt的索引
int j = 0; // pat的索引
while (i < N) {
if (pat.charAt(j) == txt.charAt(i)) {
j++;
i++;
}
if (j == M) {
System.out.println(i - j + 1);
j = lps[j - 1];
}
else if (i < N && pat.charAt(j) != txt.charAt(i)) {
if (j != 0) {
j = lps[j - 1];
} else {
i = i + 1;
}
}
}
}
// 计算LPS(Longest Prefix Suffix)数组
public static int[] computeLPSArray(String pat) {
int len = 0;
int i = 1;
int M = pat.length();
int[] lps = new int[M];
lps[0] = 0; // lps[0]总是0
while (i < M) {
if (pat.charAt(i) == pat.charAt(len)) {
len++;
lps[i] = len;
i++;
} else {
if (len != 0) {
len = lps[len - 1];
} else {
lps[i] = len;
i++;
}
}
}
return lps;
}
}
例题2:
Many Equal Substrings - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int n = sc.nextInt();
int k = sc.nextInt();
sc.nextLine();
String str = " " + sc.nextLine();
char[] a = str.toCharArray();
int[] kmp = new int[51];
// 计算KMP表
int i = 2, j = 0;
while (i <= n) {
while (j > 0 && a[i] != a[j + 1]) j = kmp[j];
if (a[i] == a[j + 1]) kmp[i++] = ++j;
else kmp[i++] = j;
}
for(i = 1; i <= n; i++) {
System.out.print(a[i]);
}
for(i = 2; i <= k; i++) {
for(j = kmp[n] + 1; j <= n; j++) {
System.out.print(a[j]);
}
}
例题3:
import java.util.*;
public class Solution {
// KMP字符串匹配算法
public int kmp (String S, String T) {
// 获取字符串和模式串的长度
int m=S.length(),n=T.length();
// 特殊情况处理
if(m>n||n==0) return 0;
// 初始化匹配次数计数器,准备next数组
int cnt=0;
int[] next=getNext(S);
// 遍历主字符串和模式串进行匹配
for(int i=0,j=0;i<n;i++){
// 当字符不匹配时,利用next数组找到下一位置
while(j>0&&T.charAt(i)!=S.charAt(j)){
j=next[j-1];
}
// 当前字符匹配成功,移动模式串索引
if(T.charAt(i)==S.charAt(j)) j++;
// 完整匹配一次模式串
if(j==m){
// 匹配次数增加,模式串索引按next数组回退
cnt++;
j=next[j-1];
}
}
return cnt;
}
// 构建next数组
private int[] getNext(String S){
int m=S.length();
int[] next=new int[m];
for(int i=1,j=0;i<m;i++){
// 当前后缀字符不匹配时,回退
while(j>0&&S.charAt(i)!=S.charAt(j)){
j=next[j-1];
}
// 当前后缀字符匹配,继续
if(S.charAt(i)==S.charAt(j)) j++;
// 设置next值
next[i]=j;
}
return next;
}
}
例题4:
import java.util.Scanner;
public class Main {
public static int ans;
public static int[] next(char [] s) {
int[] next = new int[s.length];
next[0]=0;
int j=0;
for(int i=1;i<next.length;i++) {
while (j>0 && s[i]!=s[j]) j=next[j-1];
if(s[i]==s[j]) j++;
next[i]=j;
}
return next;
}
public static int kmp(char[] S,char[] T) {
int next[]= next(T);
int j=0;
for(int i=1;i<S.length;i++) {
while (j>0 && S[i]!=T[j]) j=next[j-1];
if (S[i]==T[j]) {
j++;
ans = Math.max(ans, j);
}
if(j>T.length-1) {
break;
}
}
return ans;
}
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
String S=scanner.nextLine();
String T = scanner.nextLine();
char []a=S.toCharArray();
char []b=T.toCharArray();
System.out.println(kmp(a,b));
}
}
例题5:
10.boarder - 蓝桥云课 (lanqiao.cn)
import java.util.Scanner;
public class StringCycle {
private static int[] next(String s) {
int len = s.length();
int[] next = new int[len];
next[0] = 0;
for (int i = 1, j = 0; i < len; i++) {
while (j != 0 && s.charAt(i) != s.charAt(j)) {
j = next[j - 1];
}
if (s.charAt(i) == s.charAt(j)) {
j++;
}
next[i] = j;
}
return next;
}
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
String s = sc.next();
int[] next = next(s);
int len = s.length();
int cir = len - next[len - 1];
if (len % cir == 0) {
System.out.println(len / cir);
} else {
System.out.println(1);
}
}
}