KMP算法详解
2016-02-06 11:38
211 查看
说道KMP算法,一定要先指出问题所在之处了,问题如下:
已知一个字符串a,b
想找到在字符串a中b首次完整出现的位置。
听到这个问题,首先想到的就是朴素算法,挨个来进行匹配,然后找到对应的答案。
代码如下:
考虑如下字符串,重复的计算就很多了,因为直到最后才最终匹配。
src:abbacabbacabbacd
target:abbacd
如果匹配如上两个字符串,就会出现大量的重复计算,所以才有了之后的优化算法
KMP算法有个核心的概念,就是next数组,该数组记录最长共有元素的长度。
以abcab为例,其next = [0,0,0,1,2]
说起来比较难以理解,就是部分匹配的值。
我们使用两个标志位,一个为主字符串的标志位,一个是模式串的标志位。
代码如下:
先不看next数组的计算方法,直观的来看KMP算法的话,我们就能理解其思想了,KMP算法虽然是2层循环叠加,但是本质上,src_index 和 target_index 都是只进行了单次遍历的,所以算法复杂度为O(m + n)
比朴素算法的O(mn)快了很多。
了解了算法思想就该了解next数组的计算原理了
看代码可能不慎明了,其实这是计算相同的前后缀来的。
以字符串abcab为例,它的next数组为[0,0,0,1,2],
其前三个字符没有公共的前后缀,所以都是0,到了第四位,模式串有了一个公共的前后缀,就是a然后其字符长度1,所以next[3] = 1
到了第五位,其公共的前后缀变成了ab,所以next[4] = 2
详细列如下:
后面的就不用说了,所有的公共前后缀均为无
而公共的前后缀可以用来计算下次的src_index的偏移距离,如果模式串在匹配到src_index位置时不匹配了,可以根据之前src_index 的下次偏移的位置应该就是src_index += target_index - l[target_index - 1]
增量为模式串已经匹配的长度与next数组的在该处值的差
其实仔细想想就可以理解该处的意义,因为l[target_index - 1]其实代表的是之前最后匹配处的公共匹配值,如果没有匹配,那么偏移的距离则正好应该为target_index
说的比较复杂,实际上就是有2个问题需要弄清楚
为何src_index的偏移需要增加l[target_index - 1]
为何target_index的偏移需要回到l[target_index - 1]
当前的位置不匹配了,但是前面的内容其实都是匹配的,而匹配的内容,刚好跟前面有重复的前缀,所以,target_index 需要回到之前前缀的后面的位置,也就是l[target_index - 1]的位置。
举例来说:
假设匹配串为abcaabcaabcab,模式串为abcab,第一次匹配,在src_index = 0,target_index = 4时出现了第一次不匹配,但是abcab的l[target_index - 1] = 1,所以src_index挪移的距离就应该为3,target_index也回到了l[target_index - 1]的位置
这样就保证了匹配总能够保证最少,可以不进行重复匹配
已知一个字符串a,b
想找到在字符串a中b首次完整出现的位置。
听到这个问题,首先想到的就是朴素算法,挨个来进行匹配,然后找到对应的答案。
朴素算法
朴素算法很简单,就是挨着遍历,但是会有很多的重复计算,算法复杂度为O(mn)代码如下:
# 朴素算法 def algorithm1(src, target): for i in range(len(src)): flag = True for j in range(len(target)): if i + j >= len(src): return -1 if src[i + j] != target[j]: flag = False break if flag: return i return -1
考虑如下字符串,重复的计算就很多了,因为直到最后才最终匹配。
src:abbacabbacabbacd
target:abbacd
如果匹配如上两个字符串,就会出现大量的重复计算,所以才有了之后的优化算法
KMP算法
KMP算法跟朴素算法是对朴素算法的一个优化,朴素算法很简单,但是会有大量的重复匹配,而KMP算法就是要把重复的匹配去掉。KMP算法有个核心的概念,就是next数组,该数组记录最长共有元素的长度。
以abcab为例,其next = [0,0,0,1,2]
说起来比较难以理解,就是部分匹配的值。
算法思想
部分匹配的实质就是其实也是KMP算法的核心思想,KMP算法的核心思想在我看来就是减少匹配的次数,将重复的匹配次数去掉,那这个信息该如何去掉呢?我们使用两个标志位,一个为主字符串的标志位,一个是模式串的标志位。
代码如下:
# KMP 算法 def algorithm2(src, target): l = make_next(target) target_length = len(target) src_length = len(src) src_index = 0 target_index = 0 while src_index < src_length: while target_index < target_length: if src_index + target_index >= src_length: return -1 if src[src_index + target_index] == target[target_index]: target_index += 1 elif target_index >= 1: src_index += target_index - l[target_index - 1] target_index = l[target_index - 1] else: src_index += 1 target_index = 0 if target_index == target_length: return src_index return -1
先不看next数组的计算方法,直观的来看KMP算法的话,我们就能理解其思想了,KMP算法虽然是2层循环叠加,但是本质上,src_index 和 target_index 都是只进行了单次遍历的,所以算法复杂度为O(m + n)
比朴素算法的O(mn)快了很多。
了解了算法思想就该了解next数组的计算原理了
next数组
看如下代码# next 数组的计算 def make_next(target): l = [0] * len(target) flag = 0 for i in range(1, len(target)): if target[i] == target[flag]: flag += 1 else: flag = 0 l[i] = flag return l
看代码可能不慎明了,其实这是计算相同的前后缀来的。
以字符串abcab为例,它的next数组为[0,0,0,1,2],
其前三个字符没有公共的前后缀,所以都是0,到了第四位,模式串有了一个公共的前后缀,就是a然后其字符长度1,所以next[3] = 1
到了第五位,其公共的前后缀变成了ab,所以next[4] = 2
详细列如下:
next值 | 模式串 | 前缀 | 后缀 | 公共部分 |
---|---|---|---|---|
2 | abcab | abca,abc,ab,a | bcab,cab,ab,a | ab |
1 | abca | abc,ab,a | bca,ca,a | a |
0 | abc | ab,a | bc,c | 无 |
而公共的前后缀可以用来计算下次的src_index的偏移距离,如果模式串在匹配到src_index位置时不匹配了,可以根据之前src_index 的下次偏移的位置应该就是src_index += target_index - l[target_index - 1]
增量为模式串已经匹配的长度与next数组的在该处值的差
其实仔细想想就可以理解该处的意义,因为l[target_index - 1]其实代表的是之前最后匹配处的公共匹配值,如果没有匹配,那么偏移的距离则正好应该为target_index
说的比较复杂,实际上就是有2个问题需要弄清楚
为何src_index的偏移需要增加l[target_index - 1]
为何target_index的偏移需要回到l[target_index - 1]
target_index的偏移
先说这个,为何要回到l[target_index - 1]的位置,其实就是因为前面所说道的前后缀的概念。当前的位置不匹配了,但是前面的内容其实都是匹配的,而匹配的内容,刚好跟前面有重复的前缀,所以,target_index 需要回到之前前缀的后面的位置,也就是l[target_index - 1]的位置。
src_index的偏移
因为l[target_index - 1]的偏移位置已经变化了,所以src_index也必须进行变化,才能够让我们匹配的位置对应起来。举例来说:
假设匹配串为abcaabcaabcab,模式串为abcab,第一次匹配,在src_index = 0,target_index = 4时出现了第一次不匹配,但是abcab的l[target_index - 1] = 1,所以src_index挪移的距离就应该为3,target_index也回到了l[target_index - 1]的位置
这样就保证了匹配总能够保证最少,可以不进行重复匹配
相关文章推荐
- KMP算法详解
- 2015阿里校招前端在线题目
- Android Studio的代码混淆
- hql中不能写count(1)能够写count(a.id)
- JAVA:从public static void main(String args[])開始
- C/S中配置Log4Net(两种方式)
- 关于 sqlite3_key 和 sqlite3_rekey
- Android 中实现分享和第三方登陆---以新浪微博为例
- 岁尾年头感怀
- list()和listFiles
- 彻底防止Windows 7/8/8.1升级更新到Windows 10
- [Excel VBA]如何自动关闭MsgBox?
- C#高性能TCP服务的多种实现方式
- 计算机的前世
- Java Magic. Part 3: Finally
- 如何查看网站的端口
- js操作url的常用函数
- 给定入栈序列判断给定出栈序列是否合法(Exercise1_3_45,java)
- 消息队列
- Java线程池架构原理和源码解析(ThreadPoolExecutor)