KMP学习之旅
说起kmp就要从字符串的匹配说起,下面我们谈谈字符串的匹配
给定一个原字符串:bababababababababb,再给定一个模式串:bababb,求模式串是否在源字符串中出现
最简单的方法就是遍历源字符串,再遍历模式串,依次进行对比。当遇到不匹配的字符时源字符串和模式串返回下一个位置重新开始匹配,复杂度为n^2。
普通方法其实有可以优化的地方,假设源为字符串Q[1...n],模式串M[1...m]。
按照普通方法Q和M从i,j处进行匹配,当在k位置发生不匹配时,Q返回i+1位置,M返回开始0处重新匹配。
babab|ababababababb -- i = 5
babab|b -- j = 5 此处不匹配
b|ababababababababb -- i = 2
bababb -- j = 0 此处不匹配
bababababababababb
bababb 此处匹配完成
我们从这里思考一下,上式当开始匹配到i=5,j=5时发生了不匹配,下一步就是i返回到i=1,j=0重新匹配,其实i不用返回到1,试想一下Q[1..5]和M[1...5]是匹配的
那么Q[2...5]和M[1...5]是不匹配的,除非Q[1] = Q[2] Q[2] = Q[3] .... 同理Q[3...5]和M[1...5]匹配Q[1] = Q[2] = Q[3] ...,这么说来其实i也不用回溯了,这样j返回到正确的位置就可以继续匹配,那j这个位置怎么确定呢?
j位置无非就是使得Q[i-j, i] = M[1..j] 成立的位置,然后从这个位置继续往下匹配,中间略过的一定是不匹配的,不信自己可以证明一下。再想想刚才说的假设Q[1..i]和M[1...i]是匹配的即 Q[1...i] = M[1...i] 取局部i-j使得Q[i-j...i] = M[i-j...i] 把 Q[i-j, i] = M[1..j]带入得到 M[i-j...i] = M[1...j] 就是说要找的一个位置j使得开始j个长度的字符串等于结尾j长度的字符串。为了不漏掉匹配字符串的长度尽可能长,我们用k表示可匹配的字符串的长度则上述描述变为如下形式
找到 max(k) s.t. M[i-k...i] = M[1...k] 突然发现简单了,可以问题又来了怎么找到这样的k呢?回答是枚举呗k=1,k=2,k=3...找最长的成立的那个代码如下
1 def NEXT(pattern): 2 Next = [0] * (len(pattern)+1) 3 Next[0] = -1 4 pattern_len = len(pattern) 5 6 for i in range(pattern_len+1): 7 k = 0 8 for j in range(i): 9 if pattern[0:j] == pattern[i-j:i]: 10 k = j 11 Next[i] = k 12 13 return Next
好像又回到了出发点还是n^2的复杂度,不行不能这样堕落啊再想想。用Next表示当前模式串改回溯的位置初始化Next[0] = -1
当M[i-k...i] = M[1...k]已经成立,那么Next[i] = k ,当M[k+1] = M[i+1] 时M[i-k...i+1] = M[1...k+1] 因此M[k+1] = M[i+1] 时Next[i+1] = k + 1
当M[i-k...i] != M[1...k]时,k一定是要缩小到,怎么缩小呢?是不是要缩到某个k1的值 s.t. M[i-k1...i] = M[1...k1],如果还不匹配怎么办,继续缩小啊,是不是发现规律了对k = Next[k]不停向前缩小直到找到某个k2的值s.t. M[i-k2...i] = M[1...k2] 然后 Next[i+1] = k2。以上就是Next的递归求解复杂度只要O(m)啊,直接秒杀n^2
1 def NEXT(pattern): 2 Next = [0] * (len(pattern)+1) 3 Next[0] = -1 4 pattern_len = len(pattern) 5 6 for i in range(pattern_len): 7 j = i 8 while j > 0: 9 if pattern[Next[j]] == pattern[i]: 10 Next[i+1] = Next[j]+1 11 break 12 else: 13 j = Next[j] 14 15 return Next
Next的使用我就不讲解了,和普通字符串的回溯一样,只不过每次回溯到值都是Next里面的值,将复杂度降低到了O(m+n),代码全部明天贴