算法:模式匹配之KMP算法
前言:
昨天看到《算法导论》里的第32章:字符串匹配,说到一个关于字符串匹配的很好的算法——KMP。关于KMP的内存含意以及KMP的来源,不是本文讲述的范畴,请感兴趣的读者自行查阅相关资料。
本文主要是来说明KMP算法的思路和实现过程,以及它相比于朴素的字符串模式匹配存在的优势。
本文链接:http://blog.csdn.net/lemon_tree12138/article/details/48488813 -- 编程小笙
--转载请注明出处
朴素模式匹配算法:
1.思路分析
朴素的字符串模式匹配就是传统的算法——逐个比较。因为使用了嵌套循环,所以效率比较低。
2.代码实现
public class SimpleMatching { /** * 采用朴素的字符串匹配算法查找子字符串 * * @param T * 主字符串 * @param P * 匹配模式字符串 * @return * 匹配成功的所有位置 */ public List<Integer> getIndexOfPinT(String T, String P) { if (Tools.isEmptyString(T) || Tools.isEmptyString(P)) { return null; } List<Integer> indexs = new ArrayList<Integer>(); char[] t = T.toCharArray(); char[] p = P.toCharArray(); for (int i = 0; i <= t.length - p.length; i++) { for (int j = 0; j < p.length; j++) { if (t[i + j] == p[j]) { if (j == p.length - 1) { indexs.add(i); } continue; } break; } } return indexs; } }
我们假定主字符串的长度为n,匹配模式字符串的长度为m。那么对于上面的算法,时间复杂度就是O(m*n)。对于一些需求不太严苛或是m,n比较小的情况下。这种算法还是可以接受的。但是如果不是上述情况,这样的一个时间复杂度可能还是略显尴尬。下面我就来介绍一下改进过后的KMP算法。
KMP模式匹配算法:
1.思路分析
KMP算法的关键是为我们排除了一些重复匹配,使用主字符串的匹配位置“指针”不需要回溯。这里不妨列举一个小例子。
主字符串T:fababadaaswababaca
匹配模式P:ababaca
假使此时我们正在匹配T的第7位(fababa[d]aaswababaca)和P的第6位(ababa[c]a)。而且匹配失败了。针对朴素的匹配模式是T的“指针”回溯到(fa[b]abadaaswababaca),P的“指针”回溯到([a]babaca)。这无疑是浪费了很多的时间。
我们重新检查一下P(ababaca),当我们开始匹配第6位的时候,之前的5位已经匹配完成。而且,[aba]baca = ab[aba]ca!那么针对于T而言,fab[aba]daaswababaca这三位是已经匹配过了,我们在把与之前匹配相等的字符串移至此处时,是不是就说明,这几个字符串是不需要再匹配了。
当我们知道了在匹配的过程中,有一些字符是不需要再匹配了的时候,接下来就是重头戏了。如何让这些已经匹配过的字符串不再重复匹配?
通过上面的分析,其实已经暗含了解决方案。就是我们要知道匹配模式中,每个最优前缀(关于最优前缀可以参考《算法导论》32章内容)S中,S的不为自身的最长的一个等于最优后缀(关于最优后缀可以参考《算法导论》32章内容)的最优前缀SS。这句话可能听起来有一些绕口,下面通过一个实例来说明:
匹配模式P:ababaca
我们选取P的一个最优前缀S = ababa,那么SS = aba.因为SS是S的最优前缀,也是S的最优后缀,而且是最长的。
上面就是关于KMP匹配模式的思路分析,如果你感觉这里有一些枯燥乏味或是阅读困难(对此,我对我拙劣的文字描述表示一些歉意)。下面可以在代码中寻找大家所契合的地方,因为逻辑是想通的嘛。
2.最优前缀的形式:
Index 0123456 P ABABACA Next 0012301
3.代码实现
(1)获得字符串中的每个最优前缀子字符串中的最长的最优前缀等于最优后缀的长度
/** * 获得字符串中的每个最优前缀子字符串中的 * 最长的最优前缀等于最优后缀的长度 * * @param text * 待计算的字符串 * @return * 返回最长的最优前缀等于最优后缀的长度数组 */ public int[] getNext(String text) { if (Tools.isEmptyString(text)) { return null; } int[] lengths = new int[text.length()]; for (int i = 0; i < text.length(); i++) { String sub = text.substring(0, i + 1); int maxLen = 0; for (int j = 0; j < sub.length() - 1; j++) { String subChild = sub.substring(0, j + 1); if (sub.endsWith(subChild) && subChild.length() > maxLen) { maxLen = subChild.length(); } } lengths[i] = maxLen; } return lengths; }
(2)获得字符串P在字符串T中出现的所有位置
/** * 获得字符串P在字符串T中出现的所有位置 * * @param T * 主字符串 * @param P * 匹配模式字符串 * @return * 匹配成功的所有位置 */ public List<Integer> getIndexOfPinT(String T, String P) { if (Tools.isEmptyString(T) || Tools.isEmptyString(P)) { return null; } List<Integer> indexs = new ArrayList<Integer>(); char[] t = T.toCharArray(); char[] p = P.toCharArray(); int[] next = getNext(P); int indexT = 0; int indexP = 0; while (indexT < t.length) { if (t[indexT] == p[indexP]) { indexP++; indexT++; } else { if (indexP == 0) { indexT++; } else { indexP = next[indexP - 1]; } } if (indexP == p.length) { indexs.add(indexT - indexP); indexP = 0; } } return indexs; }
参考说明:
1.《算法导论》
2.Knuth–Morris–Pratt algorithm
源码下载:
对于上面的描述,如果你还没有完全理解,可以在下面的链接中下载与本文相关的源码进行参考学习.