简单理解KMP算法
来源:互联网 发布:中秋节祝福网页源码 编辑:程序博客网 时间:2024/06/01 09:51
KMP算法是迄今为止最为高效的字符串匹配算法。当然,在KMP算法出现之前,有关字符串的匹配问题当然经过了一个漫长的探索过程。从一开始最简单的朴素字符串匹配算法,到Rabin-Karp算法,再到有限自动机算法等等,可以说任何一个伟大算法的诞生都不可能是一朝一夕之功,在它之前一定有大量的理论及实验的基础。所以,想要彻底理解KMP算法最好是从头开始,对整个字符串的匹配问题有个完整的了解。
但是,我在这篇博文中讲的却是对KMP算法最简单的理解。只能帮助大家了解KMP最基本的思路和应用。若要详细了解,推荐《算法导论》中的“字符串匹配”一节。我没有见过比这一章讲解得更详细的资料了。
所谓字符串匹配,解决的问题就是在一段文本(text)之中寻找我们要匹配的模式(pattern)。文本和模式都是由字符串构成的,模式的长度<=文本的长度。例如,模式为”aba”,文本为”abcbaba”,所谓字符串匹配就是在文本中查找模式出现的位置(一般以文本成功匹配的字段的第一个字符的位置表示),这里应该返回4。
一种比较简单的办法是朴素字符串匹配,就是一个字符一个字符去匹配。比如上面这个例子,一开始对文本和模式都是从头开始匹配,效果如下图:
我们发现,第三个字符处文本为”c”,而模式为”a”,于是匹配失败。那么接下来,自然而然就能想到,把整个模式向右平移一位,再次进行匹配:
很遗憾,这次模式的第一个字符就没能匹配成功。这样,每次向后移动一位,依次匹配,若出现某一时刻模式的全部字符都能和它当时所对应的文本匹配,则匹配成功一次;继续向后,直到模式的第一个字符对应的是文本的第(n - m + 1)个字符为止(其中,n为文本长度,m为字符串长度),匹配结束。也就是说当模式的最后一个字符对应的是文本的最后一个字符时,就自然没有必要再进行匹配了。
通过时间复杂度分析,可知朴素匹配算法的时间复杂度为
为什么呢?还是看上面的例子,第一次匹配是,模式的第三个字符没有和文本匹配,那同时也就说明了模式的前两位和对应的文本是匹配的。我们可以确定模式未匹配的那一位所对应的文本的前一位(这里就是文本的第二位)是b。而模式的第一位是a,那么,显然,a与b不同,往后移动一位让a与b匹配就是多余的,没有必要的。
那么应该往后移动几位呢?可以想象,假如模式的第 i 位不能匹配,那么,就需要移动模式,使得模式的前k位成为模式前 i - 1 位的后缀(k在此是个小于 i 的)。
先说明一下字符串的前缀,后缀:比如字符串”abcde”中, “a”, “ab”, “abc”等等都是前缀,而”cde”, “de”, “e” 等等都是后缀。也就是说,从字符串头开始截任意小于等于字符串长度的字符,就是前缀,而从后开始截任意长度就是后缀。
回到刚才的问题,为了能够实现可能的匹配,需要模式向右偏移,使得模式以“最长的头”匹配上刚才已经匹配的文本字段的尾。也就是说寻找模式的前 i -1 项的后缀中能成为模式的最长前缀的部分。而如果后缀中找不到前缀,则将模式偏移 i 位即可。话有点抽象,看看这个例子:文本”ababababc”,模式”ababc”
同样的,第一次匹配在模式的第5个字符处失败,但是此时并没有从后面一个字符开始重新匹配,而是向右移动两位,为什么是两位呢,我们可以观察一下红箭头指的两位,因为模式的第5位匹配失败,所以,现在我们看看能否在在模式前4位的后缀中找到模式的前缀,刚好,字符串”ab”可以作为模式前4位的后缀,同时也是模式的前缀(后缀中最长的前缀)。不难发现,只有这样,才能使得这一次匹配是“可能有意义”的。
换句话说,可以通过对模式本身的计算,得出一个数组,这个数组告诉我们,如果模式的某一位不能匹配的时候,应该将模式向有偏移的位数(也就是刚才说的“后缀的最长前缀”)。然后在这个位数的基础上+1就是模式应该向右移动的位数。而因为模式一般比文本短很多,所以,我们计算这个数组消耗的计算量是可以接受的,尤其是模式比文本短很多的情况下。
具体怎么去计算这个数组,我暂且搁置,先假设这个数组已经计算完成了。接下来应该如何进行字符串的匹配。
假设这个数组名字叫pi,pi[index]
表示当模式的前index位的后缀中的最大前缀的长度。比如,模式 "ababaca"
相对应的pi数组为:
pi = [0, 0, 1, 2, 3, 0, 1]
那么,若模式的第 index + 1
位与文本不匹配时,模式需要向右移动的距离就是 pi[index] + 1
可以得到以下代码:
def kmp(pattern, text): m, n = len(pattern), len(text) # j为模式的下标 j = 0 # 遍历文本 for i in range(n): # 当模式与文本不能匹配时,向右pi[j - 1] - 1位 # 也就是说,现在用模式的pi[j]位与文本匹配 # 要求j > 0的原因是如果模式的第一位都不能匹配,那就直接向右移动一格扫描文本,再从模式的第一位开始匹配即可 # 因此j = 0 且 pattern[j] != text[i]时,是什么都不用做的 while j > 0 and pattern[j] != text[i]: j = pi[j - 1] # 匹配成功,则继续模式下一位与文本下一位的比对 if pattern[j] == text[i]: j += 1 # 整个模式匹配成功,输出信息 if j == m: print("the pattern occurs at %d" % (i - j + 1)) # 一次匹配完成,重新计算偏移量 j = pi[j - 1]
代码中,我假设数组 pi
已经被提前计算出来了。
其他所有的逻辑我通过注释说明了。不再赘述。
那么怎么计算数组 pi
呢?
如果你已经理解了上面的代码,那么计算 pi
就容易了,我们只需要稍微将上面的代码改一下,改成让模式与模式自身匹配(当然是让一个模式从第1位开始与另一个模式从第2位开始匹配),将每次匹配的最多字符的长度记录下来就是所谓“后缀的最大前缀”了。
因此,我的辅助函数 helper()
如下,负责计算偏移量数组。
# 实际上是模式的前缀与模式本身匹配def helper(pattern): m = len(pattern) # pi的第1位是0,意思是如果pattern的第一个字符就不匹配的话,无偏移量(直接向右移动1位继续即可) pi = [0] k = 0 # 遍历模式,从第2位(也就是下标1开始) for i in range(1, m): # 不匹配,向右偏移,偏移量的计算还是依靠已经计算了部分的数组pi # 这种思想有点类似于动态规划,根据之前的计算结果计算新的结果 # 每次计算的k值其实是当pattern[i]与文本不能匹配时的偏移量 while k > 0 and pattern[k] != pattern[i]: k = pi[k - 1] # 匹配成功,k + 1得到最大前缀 if pattern[k] == pattern[i]: k += 1 pi.append(k) return pi
把这两段代码合成:
我省去了所有注释,让代码更清楚,就是下面的样子,一共25行
def kmp(pattern, text): m, n = len(pattern), len(text) pi = helper(pattern) j = 0 for i in range(n): while j > 0 and pattern[j] != text[i]: j = pi[j - 1] if pattern[j] == text[i]: j += 1 if j == m: print("the pattern occurs at %d" % (i - j + 1)) j = pi[j - 1]def helper(pattern): m = len(pattern) pi = [0] k = 0 for i in range(1, m): while k > 0 and pattern[k] != pattern[i]: k = pi[k - 1] if pattern[k] == pattern[i]: k += 1 pi.append(k) return pi
总的来说,思路就是这样。若有什么问题,望批评指正。
- 简单理解KMP算法
- 简单的理解KMP算法
- KMP算法的简单理解 【笔记】
- 超级简单的理解kmp算法中的next的计算
- 对kmp算法next数组的一些简单理解
- kmp的简单理解
- KMP算法理解
- 深入理解KMP算法
- KMP算法的理解
- 理解KMP算法
- KMP算法初步理解
- 从头到尾理解KMP算法
- 从头到尾理解KMP算法
- KMP算法的理解
- KMP算法理解
- KMP算法---理解
- 从头到尾理解KMP算法
- 对KMP算法理解
- java 反射-类加载器的方式访问properties文件
- TOJ 1203.Factoring Large Numbers
- POJ 1840
- poj 1200 Crazy Search
- UVA-253 Cube painting
- 简单理解KMP算法
- 多线程下载
- HDU 4455 Substrings
- PE文件操作-末尾添加节
- GCM 调研
- 数据结构 之 线段树
- 2016年8月8号
- 安卓的架构体系
- 电脑自动安装一堆软件怎么办?