第五章 字符串(三) - 《算法》读书笔记
目录
第五章 字符串(三)
5.3 子字符串查找
- 子字符串查找:给定一段长度为N的文本和一个长度为M的模式(pattern)字符串,在文本中找到一个和该模式相符的子字符串
5.3.2 暴力子字符串查找算法
- 在文本中模式可能出现匹配的任何地方检查匹配是否存在
最坏情况下,暴力子字符串查找算法在长度为N的文本中查找长度为M的模式需要~NM次字符比较。
- 除了普通的枚举每个子串的实现方式外,还有一种显式回退的实现方式:
public static int search(String pat, String txt){
int j, M = pat.length();
int i, N = txt.length();
for(i = 0, j = 0; i < N && j < M; i++){
if(txt.charAt(i) == pat.charAt(j))
j++;
else{
i -= j;
j = 0;
}
}
if(j == M) return i - M; //找到匹配
else return N; //未找到匹配
}
5.3.3 Knuth-Morris-Pratt子字符串查找算法
- 当出现不匹配时,我们能知晓一部分文本的内容,就可以利用这些信息,避免将指针回退到所有这些已知的字符之前
5.3.3.1 模式指针的回退
- 使用一个数组dfa[][]来记录匹配失败时模式指针j应该回退多远
- 对于每个字符c,在比较了c和pat.charAt(j)之后,dfa[c][j]表示的是应该和下个文本字符比较的模式字符的位置
- 在匹配时会继续比较下一个字符,即dfa[pat.charAt(j)][j]总是j+1
5.3.3.2 KMP查找算法
- 计算出dfa[][]数组后,只需将j设置为dfa[txt.charAt(i)][j],并将i加1即可
5.3.3.3 DFA模拟
- dfa[][]数组定义的是一个确定有限状态自动机(DFA)
- 模式中的每个字符都对应一个状态
- 状态转换中只有一条是匹配转换,其他都是非匹配转换
- 如果自动机从状态0开始,到达了停止状态M,那么就在文本中找到了和模式相匹配的一段子字符串,称为确定有限状态自动机识别了该模式
- 模拟自动机运行的代码如下:
public int search(String txt){
int j, M = pat.length();
int i, N = txt.length();
for(i = 0, j = 0; i < N && j < M; i++)
j = dfa[txt.charAt(i)][j];
if(j == M) return i - N;
else return N;
}
5.3.3.4 构造DFA
- 用DFA本身构造DFA
- dfa[c][j]表示当状态为j时,匹配字符c,转移的下一个状态
- 如果当状态j匹配成功,我们进行状态推进,即
dfa[pat.charAt(j)][j] = j+1 - 如果匹配失败,我们进行状态重启,即状态回退或原地不同
- 这里定义一个影子状态X,与当前状态j拥有相同的前缀,可以通过X来获取最近的重启位置
- KMP算法的目的是要尽可能少的回退,因此状态j将当前字符委托给状态X来处理,即
dfa[][j] = dfa[][X]- 如果状态X遇见当前字符可以进行状态推进,那就在X的基础上推进
- 如果不行,说明X也需要状态重启,会去找X的影子状态,这在之前已经算好了
- 更新X的方法
X = dfa[pat.charAt(j)][X]和search方法中的j = dfa[txt.charAt(i)][j]类似,区别在于,前者是在pat中匹配pat,后者是在txt中匹配pat- 状态X总是落后状态j一个状态,具有相同前缀
- 若难以理解,参考知乎上的KMP算法详解,通过动态规划来理解KMP算法
- 具体实现如下:
dfa[pat.charAt(0)][0] = 1;
for(int X = 0, j = 1; j < M; j++){
for(int c = 0; c < R; c++)
dfa[c][j] = dfa[c][X];
dfa[pat.charAt(j)][j] = j+1;
X = dfa[pat.charAt(j)][X];
}
对于长度为M的模式字符串和长度为N的文本,KMP算法访问的字符不会超过M+N个。
- KMP算法对于在重复性很高的文本中查找重复性很高的模式很有用,但实际应用中很少有这样的情况
- 因为无需回退,KMP算法更适用于在长度不确定的输入流中进行查找,此时回退需要复杂的缓冲机制
- 如果回退很容易,我们可以用更快的算法,利用回退来获取巨大的性能收益

浙公网安备 33010602011771号