KMP算法——字符串模式匹配
什么是KMP算法?
1. 背景
KMP算法一种改进的模式匹配算法,是D.E.Knuth、V.R.Pratt、J.H.Morris于1977年联合发表,KMP算法又称克努特-莫里斯-普拉特操作。它的改进在于:每当从某个起始位置开始一趟比较后,在匹配过程中出现失配,不回溯i,而是利用已经得到的部分匹配结果,将一种假想的位置定位“指针”在模式上向右滑动尽可能远的一段距离到某个位置后,继续按规则进行下一次的比较。
2. 算法流程 (这部分读完可能会有很多问题,没关系,我们会针对每一步进行详细的讲解)
规定i是主串S的下标,j是模式T的下标。现在假设现在主串S匹配到 i 位置,模式串T匹配到 j 位置。
3. KMP只需要将j值模式串中j的位置回溯到next[j]位,而免除了前面不需要的匹配,以此来换取时间。
一. 基础问题
1、什么是字符串的模式匹配?
给定两个串S=“s1s2s3 …sn”和T=“t1t2t3 …tn”,在主串S中寻找子串T的过程叫做模式匹配,T称为模式。
2、如何寻找?
我们先从比较好理解的暴力匹配(朴素模式匹配BF算法)开始,进而引出KMP算法。
二. 暴力匹配(朴素模式匹配BF)
规定i是主串S的下标,j是模式T的下标。现在假设现在主串S匹配到 i 位置,模式串T匹配到 j 位置。
如果当前字符匹配成功(即S[i] = T[j]),则i++,j++,继续匹配下一个字符;
如果失配(即S[i] != T[j]),令i = i - (j - 1),j = 0。相当于每次匹配失败时,i 回溯到本次失配起始字符的下一个字符,j 回溯到0。
1 package com.lzp.util; 2 3 /** 4 * @Author LZP 5 * @Date 2021/3/21 22:40 6 * @Version 1.0 7 * 8 * 字符串模式匹配 9 * 暴力匹配 10 * 基本思想:用两个指针i、j去分别指向原串S与模式串P,匹配问题中只有两种情况,一种是匹配成功,另一种 11 * 是匹配不成功,如果此时i指向的原串中的字符与j指向的模式串中的字符不匹配,那么就将指针i回溯到一开始 12 * i指向的字符与j指向的字符第一次匹配成功时的下一个位置,即i = i - (j - 1),同时j也要置为0,模式 13 * 串从头开始,就一直重复这个过程,直到有其中一个指针等于其串的长度就结束该过程,最后判断此时j是否等于 14 * 模式串的长度,若等于,则表明匹配成功,反之则匹配失败。 15 */ 16 public class BF { 17 18 public static void main(String[] args) { 19 String text = "ABababaaabaBA"; 20 String pattern = "ababaaaba"; 21 // 2、开始模式匹配 22 System.out.println(BF(text, pattern)); 23 } 24 25 public static int BF(String text, String pattern) { 26 // 指向文本串的指针 27 int i = 0; 28 // 指向模式串的指针 29 int j = 0; 30 // 如果有一个指针等于了该串的长度,就跳出循环 31 while (i < text.length() && j < pattern.length()) { 32 if (text.charAt(i) == pattern.charAt(j)) { 33 // 此刻两个指针指向的字符相同,两个指针同时向后移动一位 34 i++; 35 j++; 36 } else { 37 i = i - (j - 1); 38 j = 0; 39 } 40 } 41 42 if (j == pattern.length()) { 43 return i - j; 44 } else { 45 return -1; 46 } 47 } 48 }
三. KMP算法实现
1 package com.lzp.util.kmp; 2 3 import java.util.Arrays; 4 5 /** 6 * @Author LZP 7 * @Date 2021/5/2 21:35 8 * @Version 1.0 9 * 10 * ABABCABAA 11 * 001201231 12 * -100120123 13 */ 14 public class KMP { 15 16 /** 17 * 返回一个指定模式串的next数组 18 * @param pattern 19 * @return 20 */ 21 public static int[] prefixTable(String pattern) { 22 char[] c = pattern.toCharArray(); 23 int n = c.length; 24 25 int[] next = new int[n]; 26 27 // init 28 next[0] = 0; 29 int i = 1; 30 // len代表当前最长公共前后缀 31 // 我的理解:一个字符没有公共前后缀这个说法,所以为了后面计算方便,我们默认第一个字符的公共前缀和为0 32 int len = 0; 33 while (i < n) { 34 if (c[len] == c[i]) { 35 // 相等,比较好处理 36 len++; 37 next[i] = len; 38 // 指针i向后移动 39 i++; 40 } else { 41 // 这里需要判断一下,防止数组下标越界 42 if (len > 0) { 43 // 不相等,回溯 44 // 斜着移动 45 len = next[len - 1]; 46 } else { 47 // 防止死循环,加一个else处理 48 // 当第二个字符与第一个字符不相同时,直接将第二个字符对应的next数组的值设置为0,不需要再回溯了 49 // 进入该else循环表示:此时指针指向第二个字符,即i = 1,且第二个字符与第一个字符不相同 50 next[i] = 0; 51 // 指针往后移动继续判断 52 i++; 53 } 54 } 55 } 56 57 return next; 58 } 59 60 /** 61 * kmp算法,返回模式串在主串中第一次出现位置的下标 62 * @param src 63 * @param pattern 64 * @return 65 */ 66 public static int kmp(String src, String pattern) { 67 int[] next = prefixTable(pattern); 68 movePrefix(next); 69 70 // kmp 71 int n = src.length(); 72 int m = pattern.length(); 73 int i = 0, j = 0; 74 while (i < n && j < m) { 75 if (src.charAt(i) == pattern.charAt(j)) { 76 i++; 77 j++; 78 } else { 79 // 回溯模式串 80 j = next[j]; 81 if (j == -1) { 82 // 表示此时模式串的第一个字符与当前指针i指向的字符不相等,所以此时i也需要往后移动一位 83 i++; 84 j++; 85 } 86 } 87 } 88 if (j == m) { 89 return i - m; 90 } 91 return -1; 92 } 93 94 /** 95 * 将模式串的next数组中的所有数都后移一位(除了第一个数) 96 * 这样一来就把原先next数组中的最后一个数给覆盖掉了 97 * @param next 98 */ 99 public static void movePrefix(int[] next) { 100 int n = next.length; 101 for (int i = n - 1; i > 0; i--) { 102 next[i] = next[i - 1]; 103 } 104 next[0] = -1; 105 } 106 107 public static void main(String[] args) { 108 String src = "ABABCABABDOGSDAABABCABAA"; 109 String pattern = "ABABCABAA"; 110 System.out.println(kmp(src, pattern)); 111 } 112 }
运行结果
四. 补充
什么是最长公共前后缀?
字符串的前缀是指不包含最后一个字符的所有以第一个字符开头的连续子串;后缀是指不包含第一个字符的所有以最后一个字符结尾的连续子串。例如对于字符串 abacaba,其前缀有 a, ab, aba, abac,abaca, abacab,后缀有bacaba, acaba, caba, aba, ba, a。最长公共前后缀就是 aba。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· C#/.NET/.NET Core优秀项目和框架2025年2月简报
· DeepSeek在M芯片Mac上本地化部署
· 葡萄城 AI 搜索升级:DeepSeek 加持,客户体验更智能