子字符串查找算法(BF、KMP、BM、RK)
BF 子字符串查找算法
在长度为 N 的文本中查找长度为 M 的模式
暴力子字符串查找
- 指针 i 遍历文本串每次匹配的起始位置
- 指针 j 遍历模式串中的字符,每次重置为 0
- 比较文本串中的第 i+j 个字符和模式串中第 j 个字符是否匹配
最坏情况:文本串和模式串都是一连串的A接单个B的形式,则每次都在模式串的最后一个字符处匹配失败。可能的匹配位置为 N - M + 1,每次都需要匹配模式串中的所有字符,则总比较次数为 M(N-M+1)。
// 暴力字符串查找算法
// 可能匹配的位置为: txtLen - patLen + 1
static int BruteForceSubStringSearch(String pat, String txt) {
// 指针 i 跟踪文本串中的单个字符, 表示每次匹配的起始位置
for (int i = 0; i < txtLen - patLen + 1; i++) {
// 指针 j 跟踪模式串中的单个字符
int j;
for (j = 0; j < patLen; j++) {
// 比较文本串中的字符和模式串中的字符是否匹配, 不匹配时跳出循环
// i+j: 文本串中从 i 开始的第 j 个字符
if (txt.charAt(i + j) != pat.charAt(j)) {
break;
}
}
// 如果模式串匹配结束, 则保存该次模式串出现位置的起始下标
if (j == patLen) {
return i;
}
}
return txtLen;
}
显式回退的暴力子字符串查找
-
指针 i 遍历文本串每次匹配的位置
-
指针 j 遍历模式串每次匹配的位置
-
如果匹配,则两个指针同时 +1;
如果不匹配,则文本串指针回退到本次匹配的起始位置,模式串指针回退到模式的开头
// 显式回退的暴力子字符串查找
static int ExplicitFallbackSubStringSearch(String pat, String txt) {
int i, j;
// i 为每次匹配的字符, 等价于上段代码中的i + j
for (i = 0, j = 0; i < txtLen && j < patLen; i++) {
// 文本串中的字符和模式串中的字符串相等时, 文本串和模式串中的指针同时后移
if (txt.charAt(i) == pat.charAt(j)) {
j++;
} else {
// i 在遍历的过程中指针文本中已经匹配的字符序列的末端
// 指针字符不匹配时, 需要同时回退这两个指针
// i-j 指向本次匹配的起始位置, 在下一次循环时指向后一个位置
i -= j;
// j 指向模式的开头
j = 0;
}
}
// 如果模式串匹配结束, 则保存该次模式串出现位置的起始下标
if (j == patLen) {
return i - patLen;
// 未找到匹配
} else {
return txtLen;
}
}
KMP 子字符串查找算法(DFA)
最大前后缀长度
例如串 abcdef:
-
前缀(不包括尾字符):a、ab、abc、abcd、abcde
-
后缀(不包括首字符):f、ef、def、cdef、bcdef
-
最大前后缀长度(前缀和后缀中最大的相同串的长度):0(因为其前缀与后缀没有相同的)
例如串 ababa:
- 前缀:a、ab、aba、abab
- 后缀:a、ba、aba、baba
- 最大前后缀长度:3(aba,长度3)
模式指针的回退
固定文本串不动,从左到右滑动模式字符串直到所有重叠的字符都相互匹配(或没有相匹配的字符)时才停止。在这个过程中,文本指针 i 不会回退,模式指针 j 使用数组 dfa 来更新。
i = i+1
j = dfa[txt.charAt(i)] [j]
dfa[文本串中指针 i 对应的字符 c ] [模式串中的指针 j ]:
-
文本串的第 i 个字符 c 和模式串的第 j 个字符比较之后,模式串中应该和 文本串的下一个(第 i+1 个)字符 比较的 字符位置
-
表示比较后模式字符串的指针索引,同时为已匹配字符的个数
-
匹配时:
文本串:AABAABAAAA
模式串:AABAAA
-
txt.charAt(i) == pat.charAt(j),即模式串的前 j 个字符和文本串对应相同
则应该继续比较模式串和文本串的下一个字符
-
j = dfa[txt.charAt(i)] [j] = j+1 ,即文本串的第 i+1 个字符和模式串的第 j+1 个字符比较
-
-
不匹配时:
文本串:AABAABAAAA
模式串:AABAAA
-
txt.charAt(i) != pat.charAt(j),即模式串的前 j-1 个字符和文本串对应相同
即已知文本串的 [i-j+1, … , i-1, i] 个字符
-
j = dfa[txt.charAt(i)] [j] = 模式串(从位置 1 开始)和文本串(到位置 i 结束)的最大前后缀长度
文本串:AABAABAAAA
模式串: AABAAA
文本串取后缀 [i-j+2, … , i-1, i],模式串取前缀[1, … , j-1],找最大前后缀长度
-
总结:
-
在匹配失败时,如果 模式字符串中的某处 可以和 匹配失败处的正文 相匹配(最大前后缀),那么就不应该完全跳过所有已经匹配的所有字符(i = i-j+1 和 j = 0为跳过)
-
i = i+1
- 匹配成功:j = dfa[txt.charAt(i)] [j] = j+1
- 匹配失败:j = dfa[txt.charAt(i)] [j] = 模式串和文本串的最大前后缀长度
在匹配失败时总是能够将 j 设为某个值以使 i 不回退
-
需要提前判断如何重新开始查找,而这种判断只取决于模式本身
匹配失败时,模式串的前 j-1 个字符和文本串对应相同,则由模式串自身可以得到重新回退的位置
KMP查找算法
继续比较的字符指针 | 文本串 i | 模式串 j |
---|---|---|
匹配成功 | i = i+1 | j = j+1 |
匹配失败(暴力) | i = i-j+1 | j = 0 |
匹配失败(KMP) | i = i+1 | j = dfa[txt.charAt(i)] [j] |
为什么使用 dfa[] [] 记录指针 j 后,指针 i 不用回退?
当 i 和 j 所指向的字符匹配失败时,如果是从文本串 i-j+1 处(模式串头部)开始继续检查匹配情况
则文本串中可能和模式串匹配的下一个位置应该为 i-dfa[txt.charAt(i)] [j]
又文本串中从该位置开始的 dfa[txt.charAt(i)] [j] 个字符和模式串的前 dfa[txt.charAt(i)] [j] 应该相同
因此将 j 设为 dfa[txt.charAt(i)] [j] 后,模式串的前 j 个字符和文本串已经匹配,
则无需回退指针 i ,只需要将 i 设为 i+1 ,继续比较即可
这与指针 i 和 j 所指向的字符匹配成功时的行为相同
DFA模拟
DFA(确定有限状态自动机):
-
每个模式字符串都对应着一个自动机(由保存了所有转换的 dfa[] [] 数组表示)
-
状态:数字标记的圆圈(每个状态都表示模式字符串中的索引值)
开始状态:0
中间状态 j :表示已经有 j 个字符匹配
停止状态:M (模式串的长度),不会进行任何转换
-
转换:带标签的箭头(文本串对应字母表中的每个字符)
只有一条是匹配转换(即从 j 到 j+1,标签为 pat.charAt(j))
其他的都是非匹配转换(指向左侧)
所有状态转换都和字符比较相对应
自动机运行过程:
-
从状态 0 开始,每次从左向右在文本中读取一个字符并移动到一个新的状态
-
停留在状态 0 并扫描文本,直到找到一个和模式首字母相同的字符,移动到下一个状态并开始运行
-
当状态 j 检查文本中的第 i 个字符时,沿着转换 dfa[txt.charAt(i)] [j] 前进
-
匹配转换:j = dfa[txt.charAt(i)] [j] = j+1,向右移动进入下一个状态
等价于增大模式字符串的指针 j
-
非匹配转换:j = dfa[txt.charAt(i)] [j] = 最大前后缀长度,向左移动回到较早的状态
等价于将模式字符串的指针 j 变为一个较小值
并继续检查下一个字符(i+1)
-
-
结果判断
-
到达状态 M :识别到该模式(从文本中找到了和模式相匹配的一段子字符串)
-
文本结束时未达到状态M,则文本中不存在匹配该模式的子字符串
-
KMP子字符串查找算法(DFA模拟):
// 在 txt 中找到 pat 的出现位置
public int search(String txt){
int i, j, N = txt.length(), M = pat.length();
for(i=0, j=0; i<N && j<M;i++){
j = dfa[txt.charAt(i)][j];
}
// 找到匹配
if(j