我从对暴力匹配算法的优化角度出发,理解和构造出KMP算法。
模式匹配问题中,有主串,子串,模式串,前缀,后缀,部分匹配值等等概念。教材往往”反着来“,先有解决方案和概念,再强行说”啊,就是这样子的“。但我们都应该清楚,概念是辅助思考,辅助解决遇到的问题的。
假设我们已经弄懂了暴力匹配解决模式匹配的方法,那么也就清楚主串、子串和模式串的概念。
现在,想办法对暴力匹配算法进行优化。所谓”暴力匹配“,就是它穷举遍历了主串的”子串空间“,即每一个子串都去对比一次。但是很多情况,子串部分匹配了模式串,”眼看着就要匹配成功了“,却失配了。针对这些情况,暴力匹配算法都是采取直接复位主串指针,”就好像一切都没发生过“。
同样极端的,我们也可以采取”不后悔“的策略。即下图的”简单策略“,直接跳过那些被部分匹配的字符。
但很不幸,这样做存在”错过对的子串“的可能性。
这让我们反思:为什么部分匹配失败的字符却组成了匹配成功的子串?
这个例子帮助我们发现了前面的断言太过于决定,只有那些没有”内在模式“的部分匹配字符才能被跳过。那么那个”内在模式“是什么呢?
为了理解这种无法简单跳过的情况,我们有了”前缀“和”后缀“等概念。同时也发现,那些部分匹配的情况,我们不需要考虑主串,直接可以用模式串来分析。
这些”重复出现的前缀“就是我们要寻找的”内在模式“。因此我们要分析模式串的每一个前缀,同时他们的长度和数组下标一一对应,那么我们只需要分析这些”重复出现的前缀“的长度即可,于是我们定义部分匹配值,为能在前缀里找到的最长的”内在模式“的长度。
可上图说的是跳转?那是因为二者是等价的概念,想不明白就暂停仔细想想(下标、长度)。
于是我们目前优化暴力匹配算法的方案有了,”事前分析“模式串然后制表(next数组),根据这个表跳转模式串指针的位置,保持主串指针不复位。(从而跳过那些无限复位考虑的子串,又不会错过潜在匹配成功的子串)。
可是,如何计算next数组呢?
我们可以人工推算出部分匹配值表(考虑每一个前缀的前缀集合和后缀集合,找到二者交集的最长元素,该长度就是这个前缀的部分匹配值),然后根据等价关系换算到next数组。
但是,如果编码计算呢?毕竟程序没有提供集合运算(或者为了高效,我们也不应该采取集合运算)。没办法了,只能把每种情况,在”内存里看起来是什么样子的“画一画。看有没有思路吧。
所以我们可以从最小前缀开始,不断利用并构造next数组!用C++试试看。
#include
最后,还可以对next数组进行优化,因为还存在一种情况,跳转指向的字符与当前字符一样,那么匹配一定会失败从而继续发生next跳转(回溯),不如在构造next数组时就把这种情况考虑进去,从而完成优化。