子字符串查找算法
术语:模式和文本
在文本中找出和模式相符的子串。
子字符串查找算法:
算法 | 性能 | 备注 |
---|---|---|
暴力查找法 | 一般情况: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"));
}
}
应用场景:
- 文本串和模式串的重复性很高
- 文本串是长度不确定的输入流