子字符串查找算法
术语:模式和文本
在文本中找出和模式相符的子串。

子字符串查找算法:
算法 | 性能 | 备注 |
---|---|---|
暴力查找法 | 一般情况:O(N+M);极端情况:O(NM) | 实现简单 |
KMP 算法 | O(N+M) | 确定有限状态自动机 DFA |
BM 算法 | 一般只检查文本部分字符 | 从右往左比较模式串 |
RK 算法 | O(N+M) | 哈希算法、实现简单 |
暴力子字符串查找算法
性能:
- 在极端情况下,时间复杂度是 O(MN) - (文本和模式存在大量重复字符)
- 一般情况下,时间复杂度是 O(M + N) - (一般不需要完整比较模式串)
思路:
- 枚举文本中所有和模式串长度相等的子串并进行比较
- 枚举子串时,如果文本剩余长度小于模式长度时,可提前结束查找
算法实现1:
public class BruteForce1 { /** * 在文本中查找和模式相符的子串 * @param pat pattern * @param txt text * @return 子串存在,返回字符首字符索引;否则,返回 -1 * */ public static int search(String pat, String txt) { int m = pat.length(); int n = txt.length(); for (int i = 0; i <= n - m; i++) { int j; for (j = 0; j < m; j++) { if (pat.charAt(j) != txt.charAt(i + j)) { break; } } if (j == m) { return i; } } return -1; } }
测试:
class BruteForce1Test { @Test void search() { assertEquals(0, BruteForce1.search("Hello", "Hello World")); assertEquals(6, BruteForce1.search("World", "Hello World")); assertEquals(-1, BruteForce1.search("not exists", "Hello World")); } }
思路:将文本与模式进行比较,比较失败时回退指针 i, j,对下一个子串进行比较
算法实现2:
public class BruteForce2 { public static int search(String pat, String txt) { int m = pat.length(); int n = txt.length(); int i, j; for (i = 0, j = 0; i < n && j < m; i++) { if (txt.charAt(i) == pat.charAt(j)) { // 匹配时,比较下一个字符 j++; } else { // 不匹配时,回退指针 i, j i -= j; j = 0; } } if (j == m) { return i - m; } return -1; } }
测试:
class BruteForce2Test { @Test void search() { assertEquals(0, BruteForce2.search("Hello", "Hello World")); assertEquals(6, BruteForce2.search("World", "Hello World")); assertEquals(-1, BruteForce2.search("not exists", "Hello World")); } }
总结:
- 性能问题;在极端情况下,指针 i, j 总是需要回退 m - 1 个位置,存在大量重复的比较
- 应用情况:在一般情况下,不需要完整地比较整个子串且实现简单
KMP 算法
本节重点:
- DFA 如何使用
- DFA 如何构造
全称:Knuth-Morris-Pratt 子字符串查找算法(三位发明者的名字)
基本思想:在匹配失败时,能够利用已比较的信息,将 j 重置使 i 不回退
可能性:当匹配失败时,可以通过右移模式串来避免回退指针 i
- 右移时,如果已比较的子串(后缀)和已比较的模式串(前缀)能重叠,则将模式串移到该位置
- 否则,将模式串移到和下一个子串重叠的位置
txt: AABAAB|AAAB pat: AABAAA| -> AAB|AAA
实现:对模式串进行预处理,构造 DFA(确定有限状态自动机),在遍历文本串的字符时,可以从 DFA 得到下一个 j 的值(前进或回退)
DFA: dfa[txt.chatAt(i)][j]
的值是和文本串的下一个字符 txt.chatAt(i+1)
比较的 j 值
过程:将文本字符输入到 DFA,DFA 会从一个状态转换到另一个状态,直到达到终止状态(匹配成功)或文本结束(匹配失败)
算法实现:
public class KMP { private static final int R = 256; // 字符集大小 private String pattern; private int m; private int[][] dfa; /** * 根据模式串构造确定有限状态自动机 DFA * */ public KMP(String pattern) { this.pattern = pattern; this.m = pattern.length(); dfa = new int[R][m]; // 初始化第一列 dfa[pattern.charAt(0)][0] = 1; // 初始化其他的列,从第二列开始 // 当 txt[i..i+j] 匹配失败时,从 txt[i+1] 开始匹配, // 这里可以想象成DFA正在处理第 2 个字符的匹配情况 // X 记录重启状态 for (int j = 1, X = 0; j < m; j++) { for (int c = 0; c < R; c++) { dfa[c][j] = dfa[X][j]; // 匹配失败,将 dfa[][X] 复制到 dfa[][j] } dfa[pattern.charAt(j)][j] = j + 1; // 匹配成功,将 dfa[pattern.charAt(j)][j] 设置成 j + 1 X = dfa[pattern.charAt(j)][X]; // 更新 X,即从 txt[i+1] 开始匹配或者说在构建自动的同时也在使用自动机 } } /** * 在文本串中查找子串 * 将文本串的字符逐个放到 DFA 中,直到 DFA 达到终止状态或文本串结束 * */ public int search(String txt) { int n = txt.length(); int i, j; for (i = 0, j = 0; i < n && j < m; i++) { j = dfa[txt.charAt(i)][j]; } if (j == m) { return i - m; } return -1; } }
测试:
class KMPTest { @Test void search() { KMP kmp = new KMP("Hello"); Assertions.assertEquals(0, kmp.search("Hello World")); Assertions.assertEquals(4, kmp.search("Say Hello")); Assertions.assertEquals(-1, kmp.search("hello world")); } }
应用场景:
- 文本串和模式串的重复性很高
- 文本串是长度不确定的输入流
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 25岁的心里话
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现