算法具体思路:
1 求出子串的模式匹配串(pattern)长度,记录在数组 A[i] 中:
如上图记录了每一位的模式匹配串("前缀"和"后缀"的最长的共有元素的长度)的长度。利用动态规划的思想:
从匹配串的概念理解,前缀和后缀的最长的共有元素长度,这说明每一位记录的这个长度就是当前这一位的字符所能匹配到的最长前缀的长度(也就是下一次应该去比较的索引),所以我们可以构造动态方程:
A[0] = 0, A[i] = str.charAt(A[i - 1]) == str.charAt(i) ? A[i - 1] + 1 : str.charAt(i) == str.charAt(0) ? 1 : 0
这样我们可以利用O(m)的时间复杂度构构造出pattern数组 A[i].
2 开始匹配母串,利用pattern 数组避免重复比较:
思想: 设定两个指针, i (母串指针);j(匹配串指针);母串指针 i从头到尾依次扫过(匹配时 ++, 否则不动,当匹配值为0时仍然失配才向后移动一位)。匹配串指针 j 在失配时移动到A[j - 1] 的位置继续匹配。几个问题如下:
(1) 为什么要移动到A[j - 1]:
首先我们来看一下部分匹配值的含义:当我们求匹配值的时候已经用到了这个思想。每一个位置记录的是当前这个字符所能匹配到的最长的前缀长度,也就是下一次应该去比较的地方。所以说明我们失配的时候,要去寻找子串下一次应该去比较的地方,也就是我们求出来的A[j - 1 ]的位置。也就是子串从开头到A[j - 1] - 1的位置已经可以和之前比较过的位置对齐相等。
(2) 这样可以优化多少复杂度:
首先我们看一下之前naive matching的思想:每次不匹配母串的指针需要回退到这次匹配开始的位置加1,子串回到初始位置重新开始新的匹配。也就是说:近似看出母串从m 到 n - m这些位置的字符如果在最坏的情况下(每次都是比较到最后一个发现失配,有点背啊),都要比较m次。而前m个和后m个加起来平均每一位比较了m/2次。 所以复杂度为 O((n - m + 1) * m),随后我们来看一下KMP我们发现就算失配了,我们只是调整子串的指针,而母串的指针只是在原地等待并没有回退(直到匹配了或者匹配值为0仍然失配,这时候才移动);我们还是来看最坏的情况:给出这样的子串 aaaaaa, 母串aaaaabaaaaabaaaaabaaaaaa, 我们发现每次都是匹配到最后一位失配并且母串这一位的母串需要与子串前m - 1个字符都比较一遍发现都不行才会向后移动一位。原因是我们的部分匹配值是一个连续的值(0,1,2,3,4,5). 就算这样复杂度:O(n + (n / m)*(m - 1)) = O(2n)。为n的量级。优化了很多。
3 进一步优化KMP算法:
优化点:对于子串为aaaaa这种情况的优化。
我们发现上面讨论过的KMP算法的最坏的情况有大量的不需要比较的地方,比如由于子串全都是相等的字符,当最后一位失配时我们可以直接将母串的指针向后移动一位同子串第一位重新匹配。所以这是一个可以省略大量不必要的比较的地方。我们对字符全相等的子串的部分匹配值进行研究发现当子串全部字符都相等时会出现A[ j ] = j 的规律,所以当我们失配时可以判断一下A[ j ] == j ? j = 0, i ++ : j = A[j - 1].这样我们可以避免重复比较,真正实现O(n)的复杂度 。
代码如下图:
转载请著名出处:http://www.jianshu.com/p/07e2c34a07b7