KMP经典算法与变形的应用(字符串parttern匹配问题)
1. 问题描述
求主串字符串a是否含有模式串字符串parttern b,也就是匹配问题经典KMP算法是计算next[j]数组,然后每次移动若干位而不是暴力求解的每次移动到partern的首位来比较。
暴力求解的算法很明显时间复杂度是O(m*n),而KMP(三个发明者的首字母)算法 的时间复杂度是O(m+n),为什么时间复杂度是O(m+n),以及KMP算法的
关键点next[]数组的求解的精髓——相等的最长前缀后缀串长度数组的求解在下面进行介绍。
最后还会介绍一下KMP的next数组的变形(有些情形下将会提速)。
2.暴力求解BF
主串每次移动一个位置,每次都与模式串partern进行比较,当遇到不相同元素的时候进行break结束当前循环,然后主串移动到下一个位置.....不用多说
1 def bf(s, partern): 2 start = 0 3 while start <= (len(s) - len(partern)): 4 partern_index = 0 5 for i in xrange(start, start+len(partern)): 6 if partern[partern_index] != s[i]: 7 break 8 elif partern_index == (len(partern)-1): 9 return start 10 partern_index += 1 11 start += 1 12 return -1
3. KMP经典算法
核心思想:
在比较的过程中,当遇到a[i] != b[j]的时候并不是将索引 i 移动到当前比较开始处的下一位而是移动模式串!!!这就是主串不回溯!
那么把模式串移动到哪里??这就是接下来分析的next[]
以下图片来源于july课程,不再标注
目的就是黄色元素和绿色元素不相等时候,将模式串移动若干位使得BD串=AC串,是不是节省了很多很多比较
3.1 next[]数组求解
s = 'abababcdgababcabcdcfabcabbbaabccc'
partern = 'abcabcdcfabcabbb'
求解模式串parttern的每一个元素的最大相等的前缀串和后缀串
思想:如果p[k] == p[j],那么令next[j+1] = next[j]+1
如果p[k] != p[j],那么令h = next[k];比较p[h] 与p[j]的相等关系
重复此过程,看起来像是从后向前的递归实际上求解过程是从前向后的一个求解
还是举个例子比较通俗易懂:partern = 'abcabcdcfabcabbb'
next[0] = -1
指针移动到b的位置时,很显然a自己没有所谓的前缀后缀,所以next[1] =0
指针移动到c..............,显然ab没有相等的前缀后缀..................next[2] = 0
.................a.........................abc.................................................next[3]=0
.................b.........................abca的前缀有a ab abc abca,后缀有a c bca有一个相同的就是a,长度是1,所以next[4] =1
以此类推
最终得到:
next=[-1, 0, 0, 0, 1, 2, 3, 0, 0, 0, 1, 2, 3, 4, 5, 0]
1 def get_next(partern): 2 nex = [-1] * len(partern) 3 index = 0 4 k = -1 5 while (index < len(partern) - 1): 6 if k == -1 or partern[index] == partern[k]: 7 index += 1 8 k += 1 9 nex[index] = k 10 else: 11 k = nex[k] 12 return nex
接下来就可以进行kmp的求解了。
3.2 KMP算法代码
一句话:与暴力求解的思维一样,只不过主串不回溯,根据next[]移动模式串。
1 def kmp(s, partern): 2 nex = get_next(partern) 3 index = 0 4 start = 0 5 while start < (len(partern)-1): 6 if start == -1: 7 index += 1 8 start = 0 9 continue 10 if s[index] == partern[start]: 11 index += 1 12 start += 1 13 else: 14 start = nex[start] 15 return index - start
s = 'adgababcabcdcfabcabbbaabccc'
parttern = 'abcabcdcfabcabbb'
结果是 5
3.3 KMP算法时间复杂度解释
为什么KMP的算法时间复杂度是O(m+n)
首先在求解next[]数组的时候是遍历的求,因此式m,关于递归问题上面已经说过了,真正求解是从前向后,因此前面的next值已经求出来了,所以可以忽略
在执行KMP的时候实际上也是遍历,长度是n,过程中不回溯,因此比较的次数仍然是n(平均),因此是O(m+n)
4.KMP算法变形
现在考虑这样一个问题,当s[i] == p[j]的时候i++ j++,当出现s[i] != p[j]的时候会根据next[]移动p的索引,假如说 j=10,next[j] = 2,同时p[10] == p[2]这种情况,是不是做了一次无用功,因为s[i] != p[j]已经确定了,不然不可能移动p,换句话说s[i] != p[2]所以本次移动是无用功,还得继续移动。看下图
因此在求解KMP的时候加上一个判断,如果p在j位置的元素值与要移动到k的位置的元素值是相同的,那么就移动到p[next[next[j]]] 而不是p[next[j]]
1 def kmp2(s, partern): 2 nex = get_next(partern) 3 index = 0 4 start = 0 5 while start < (len(partern) - 1): 6 if start == -1: 7 index += 1 8 start = 0 9 continue 10 if s[index] == partern[start]: 11 index += 1 12 start += 1 13 else: 14 if parttern[start] == parttern[nex[start]]: 15 start = nex[nex[start]] 16 start = nex[start] 17 return index - start
结果仍然是5
同时都调用kmp(s, parttern) 和kmp2(s, parttern)可以count一下循环的次数来比较两者的速度。
参考链接:
http://www.cnblogs.com/c-cloud/p/3224788.html
https://www.cnblogs.com/yjiyjige/p/3263858.html
等
有任何疑问请留言或者发送至邮箱397585361@qq.com