子字符串查找算法

术语:模式和文本

在文本中找出和模式相符的子串。

子字符串查找算法:

算法 性能 备注
暴力查找法 一般情况: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"));
}
}

应用场景:

  • 文本串和模式串的重复性很高
  • 文本串是长度不确定的输入流
posted @   廖子博  阅读(120)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 25岁的心里话
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现
点击右上角即可分享
微信分享提示