KMP算法
1.KMP算法简介
KMP算法是一种改进的字符串匹配算法,由D.E.Knuth,J.H.Morris和V.R.Pratt同时发现,因此人们称它为克努特——莫里斯——普拉特操作(简称KMP算法)。KMP算>> 法的关键是利用匹配失败后的信息,尽量减少模式串与主串的匹配次数以达到快速匹配的目的。具体实现就是实现一个next()函数,函数本身包含了模式串的局部匹配>> 信息。时间复杂度O(m+n)。
2.KMP算法与确定性有限状态自动机DFA
2.1 DFA与KMP算法
子串查找问题通常会存在两个字符串,一个是原串s
, 另一个是模式串p
,设m = len(s), n = len(p)
并且通常m >> n
。
对于子串查找问题,很朴素、直接的一个解法就是暴力求解法,即从s中的第0个字符开始,将原串s中的每一个字符和模式串p
的每一个字符进行比较,若全部匹配成功,则返回;否则,则从s中的第1个字符开始,重复之前的比较操作直到到达s的最后一个字符;
从上述的描述中易得出 暴力解法 的时间复杂度是O(nm),暴力解法的缺点:
- 时间复杂度高;
- 不适用于字符流的情况;当原串s是字符流(例如网络字符)的时候,该解法存在回溯,若不加额外的缓存,是不能用于此类问题的;
但是,大多数情况下,原串和模式串都比较小,也都不是字符流的情况,而采用高级算法通常都用一些预处理的过程,对于小规模问题这都是不划算的,因此暴力解法还是很常用的,比如jdk
的indexOf()
就是采用暴力解法实现的。
从暴力解法的描述可以看出,当从s的字符i开始,与模式串p逐字符比较时,若在i+k处发生失配时,指向s的指针是需要回溯到i+1继续逐字符比较,而没有利用好已经匹配好的k
个字符。
KMP算法解决的问题是:当发生字符失配的时候,不回溯指针i。这样就能克服 暴力解法 的两个缺点。
KMP算法核心的思想是:当发生字符失配的时候,充分利用已经匹配成功的k个字符的信息,避免指针i的回溯
KMP算法可以用确定性有限状态机DFA来直观的阐述。
DFA
- 包含有限的状态(包括开始和停止)
- 每一个字符只发生一次状态的转移
- 如果一系列的状态转移到了停止状态,则匹配成功
关键在于如何根据模式串p构建DFA,略。
2.2 DFA子串查找
public class DFASubStringSearchDemo {
public static void main(String[] args) {
// TODO Auto-generated method stub
char[] radixChar = new char[] {'A', 'B', 'C', 'D',
'E', 'F', 'G', 'H',
'I', 'J', 'K', 'L',
'M', 'N', 'O', 'P',
'Q', 'R', 'S', 'T',
'U', 'V', 'W', 'X',
'Y', 'Z'};
String txt = "ABCBDBCBABCBCBABBWEHJHHOISCBIIOSAOPOPIOHUCUIBSYGTWBNIOAUSABCBDBCBABCBCBABBCBABCCCAASASADSWFEFSDBCBABCCCAASASADSWFEFSDB";
System.out.println(txt.length());
String pattern = "ABC";
DFASubStringSearchDemo demo = new DFASubStringSearchDemo();
int[][] dfa = demo.buildDfa(pattern, radixChar);
int startIndex = demo.search(txt, pattern, dfa);
System.out.println(startIndex);
String target = startIndex+pattern.length() <= txt.length() ? txt.substring(startIndex, startIndex+pattern.length()) : "NO MATCH";
System.out.println(target);
}
public int[][] buildDfa(String pattern, char[] radixChar) {
int[][] dfa = new int[radixChar.length][pattern.length()];
dfa[pattern.charAt(0)-'A'][0] = 1;
for (int X = 0, j = 1; j < pattern.length(); j++) {
for (int c = 0; c < radixChar.length; c++)
dfa[c][j] = dfa[c][X];
dfa[pattern.charAt(j)-'A'][j] = j+1;
X = dfa[pattern.charAt(j)-'A'][X];
}
return dfa;
}
public int search(String txt, String pattern, int[][] dfa) {
int n = txt.length(), m = pattern.length(), i = 0, j = 0;
for (; i < n && j < m; i++)
j = dfa[txt.charAt(i)-'A'][j];//状态转移
if (j == m)
return i - m;
else
return n;
}
}
3. KMP算法的主流实现
3.1 next数组
next[j]数组表示模式串p的位置j发生失配时,应该从next[j]处继续匹配,而不用回溯原串s的i指针。
同时,next[j]的值也是表示模式p[0~j-1]的最长公共前后缀的长度。
3.2 具体实现
kmp
算法
public int kmp(String txt, String pattern, int[] next) {
int i = 0, j = 0;
for (; i < txt.length() && j < pattern.length();) {
if (j == -1 || txt.charAt(i) == pattern.charAt(j)) {
i++;
j++;
} else {//mismatch 利用next数组得到j回退的位置
j = next[j];
}
}
if (j == pattern.length())
return i - j;
else
return -1;//not found
}
关键在于构建next
数组
public int[] getNext(String pattern) {
int[] next = new int[pattern.length()];
next[0] = -1;
int k = -1;
int j = 0;
while (j < pattern.length() - 1) {
if (k == -1 || pattern.charAt(j) == pattern.charAt(k)) {
j++;k++;
if (pattern.charAt(j) == pattern.charAt(k))
next[j] = next[k];//j和k字符相同,因此j发生失配时,若跳转到k,则k也会发生失配,继而跳转到next[k],所以不如直接一步到位,将k的next值而不是k赋值给next[j];
else
next[j] = k;
} else {
k = next[k];//k backup until pattern[k] == pattern[j]
}
}
return next;
}