简介
KMP算法,全称为Knuth-Morris-Pratt子字符串查找算法。Knuth、Morris和Pratt发明的这个算法的基本思想是在当字符串出现不匹配时,就能知晓一部分文本的内容(因为在匹配失败之前它们已经和模式相匹配)。我们可以利用这些信息避免将指针回退到所有这些已知的字符之前。
举例:
例如在正文ABAAAABAAAAAAAAA中查找模式字符串BAAAAAAAAA,在匹配过程中,我们会发现当我们匹配到第六个字符时(第一个字符为B)匹配失败,文本指针现在指向的是末尾的字符B。我们可以观察到,这里不需要回退文本指针i,因为正文中的第六个字符B前有四个A,均与模式的第一个字符不匹配,另外,i当前指向的字符B和模式的第一个字符向匹配,所以可以直接将i + 1,以比较文本中的下一个字符和模式中的第二个字符,从而继续查找。
在匹配失败时,如果模式字符串中的某处可以和匹配失败处的正文相匹配,那么久不应该完全跳过所有已经匹配的字符。KMP算法的主要思想是提前判断如何重新开始查找,而这种判断只取决于模式本身。
1、模式指针的回退
在KMP子字符串查找算法中,不会回退文本指针i,而是使用一个数组dfa[ ][ ]来记录匹配失败时模式指针j应该回退多远。
对于每个字符c,在比较了c和pat.charAt(j)之后,dfa[c][j]表示的是应该和下个文本字符比较的模式字符的位置。
在查找中,dfa[txt.charAt(i)][j]是在比较了txt.charAt(i)和pat.charAt(j)之后应该和txt.charAt(i+1)比较的模式字符位置。在匹配时会继续比较下一个字符,因此dfa[pat.charAt(j)][j]总是j + 1。
在不匹配时,不仅可以知道txt.charAt(i)的字符,也可以知道正文中的前j - 1个字符,它们就是模式中的前j - 1个字符。
可与滑动窗口匹配字符串的方法进行比较。
2、KMP查找算法
只要计算出来dfa[ ][ ]数组,就得到了后面所示的子字符串查找算法(DFA模拟):
当 i 和 j 所指向的字符匹配失败时(从文本的i - j + 1处开始检查模式的匹配情况),模式可能匹配的下一位置应该从i - dfa[txt.charAt(i)][j]处开始。按照算法,从该位置开始的dfa[txt.charAt(i)][j]个字符和模式的前dfa[txt.charAt(i)][j]个字符应该相同,因此无需回退指针i,只需要将j设为dfa[txt.charAt(i)][j]并将 i 加上 1即可,这正是当 i 和 j 所指向的字符匹配时的行为。
3、DFA模拟
先上代码:
//KMP子字符串查找算法(DFA模拟)
public int search(String txt){
//模拟DFA处理文本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 == M) return i - M;//找到匹配
else return N; //未找到匹配
说明这个过程的一种较好的方法是使用确定有限状态自动机(DFA)。
这和我们在电子电路中学到的状态图很相似,看图还是很好理解的。
KMP的字符串查找方法search()只是一段模拟自动机运行的Java程序。
4、构造DFA
解决KMP算法的关键问题:如何计算给定模式相对应的dfa[ ][ ]数组?
Knuth、Morris、和Pratt发明了一种巧妙(但也很复杂)的构造方式。
dfa[pat.charAt(0)][0] = 1;
for (int x = 0, j = 1; j < M; j++){
//计算dfa[][j]
for (int c = 0; c < R; c++){
dfa[c][j] = dfa[c][X];
}
dfa[pat.charAt(j)][i] = j + 1;
X = dfa[pat.charAt(j)][X];
}
对于每个 j ,它将会:
将dfa[ ][X]复制到dfa[ ][j](对于匹配失败的情况)。
将dfa[pat.charAt(j)][j] 设为 j + 1(对于匹配成功的情况)。
更新X。
KMP字符串查找算法
public class KMP
{
private String pat;
private int[][] dfa;
public KMP(String pat)
{ //由模式字符串构造DFA
this.pat = pat;
int M = pat.length();
int R = 256;
dfa = new int[R][M];
dfa[pat.charAt(0)][0] = 1;
for (int X = 0, j = 1; j < M; j++)
{ //计算dfa[][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];//更新重启状态
}
}
public int search(String txt)
{ //在txt上模拟DFA的运行
int i, j, M = txt.length(), M = pat.length();
for (i = 0, j = 0; i < N && j < M; i++)
{
j = dfa[txt.charAt(i)][j];
}
if(j == M) return i - M;//找到匹配(到达模式字符串的结尾)
else return N;//未找到匹配(到达文本字符串的结尾)
}
public static void main(String[] args){}
}
对于长度为M的模式字符串和长度为N的文本,KMP字符串查找算法访问的字符不会超过M+N个。