By Long Luo
之前虽然知道 KMP 算法,但是一直无法深入理解其原理,最近看了 2 篇文章:从头到尾彻底理解KMP(2014年8月22日版) 和 KMP算法教程 ,然后再实际写代码,终于对 \(\textit{KMP}\) 算法有了一定了解了,下面写一写我个人的学习过程。
这篇文章主要从我个人学习过程来叙述:
为了解决什么问题?
\(\textit{KMP}\) 算法是一种字符串匹配算法,可以在 \(O(n+m)\) 的时间复杂度内实现两个字符串的匹配。
所谓字符串匹配,是这样一种问题:
- 字符串 \(\textit{P}\) 是否为字符串 \(\textit{S}\) 的子串?
- 如果是,\(\textit{P}\) 出现在 \(\textit{S}\) 的哪些位置?
其中 \(\textit{S}\) 称为主串;\(\textit{P}\) 称为模式串。
最常见的就是经常要用的搜索功能,比如要在一大段文字中找到某个句子或者找到出现的全部位置。在这种场景下,要找的句子就是给定的模式 \(\textit{P}\) ,而大段文字就是目标字符串 \(\textit{S}\) 。
从暴力法开始
我们先从最直观的地方开始:
- 两个字符串 \(\textit{A}\) , \(\textit{B}\) 的比较?
- 如果 \(\textit{P}\) 是 \(\textit{S}\) 的字串,第一个出现的位置在哪里?
所谓字符串比较,就是问两个字符串是否相等。
最朴素的思想,就是从前往后逐字符比较,一旦遇到不相同的字符,就返回 \(\textit{false}\) ;如果两个字符串都结束了,仍然没有出现不对应的字符,则返回 \(\textit{true}\) 。
代码实现如下:1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| public static int Search_BruteForce(String targetStr, String patternStr) { int targetLen = targetStr.length(); int patLen = patternStr.length();
for (int i = 0; i < targetLen; i++) { if (targetStr.charAt(i) == patternStr.charAt(i)) { for (int j = 1; j < patLen; j++) { if (targetStr.charAt(i + j) != patternStr.charAt(j)) { break; }
if (j == patLen - 1) { return i; } }
} }
return -1; }
|
或者双指针版:1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| public static int ViolentMatch(String targetStr, String patternStr) { int targetLen = targetStr.length(); int patLen = patternStr.length();
int i = 0; int j = 0; while (i < targetLen && j < patLen) { if (targetStr.charAt(i) == patternStr.charAt(j)) { i++; j++; } else { i = i - j + 1; j = 0; } }
if (j == patLen) { return i - j; } else { return -1; } }
|
暴力解法复杂度分析
刚才我们成功实现了暴力算法,那么其时间复杂度如何?
记 \(n\) 为串 \(\textit{S}\) 的长度,\(m\) 为串 \(\textit{P}\) 的长度。
考虑“字符串比较”这个小任务的复杂度。最坏情况发生在:两个字符串唯一的差别在最后一个字符。这种情况下,字符串比较必须走完整个字符串,才能给出结果,因此复杂度是 \(O(len(str))\) 的。
由此,不难想到暴力算法所面对的最坏情况:
主串形如 “AAAAAAAAAAAAA…B” ,而模式串形如 “AAAAA…B” 。每次字符串比较都需要付出 \(O(m)\) 次字符比较的代价,总共需要比较 \(n\) 次,因此总时间复杂度是 \(O(nm)\) 。
那么如何改进呢?
信息熵冗余
我们很难降低字符串比较的复杂度(因为比较两个字符串,真的只能逐个比较字符)。因此,我们考虑降低比较的趟数。如果比较的趟数能降到足够低,那么总的复杂度也将会下降很多。
要优化一个算法,首先要回答的问题是“我手上有什么信息?”我们手上的信息是否足够、是否有效,决定了我们能把算法优化到何种程度。请记住:尽可能利用残余的信息,是 \(\textit{KMP}\) 算法的思想所在。
很明显在暴力算法中,模式字符串每次都需要比较,非常复杂,那么这里面有没有优化的时间呢?