1 目标
搞任何算法前我们需要搞清楚目标,我们的目标是:
- 给定一个文本串和一个模式串,从文本串里面找第一次出现的模式串。
- 特别地,找不到就返回-1,模式串为空则返回。
- 例如:kmp(‘aadaadaaf’, ‘aadaaf’) = 3
2 概念
我们学习模式串匹配文本串之前,必须明确几个重要的概念:
- 文本串:一个长字符串,里面可能有模式串,如aadaadaaf。
- 模式串:一个比文本串短的字符串,它可能蕴含在文本串中,如aadaaf。
- 前缀:一个字符串的子串(去除最后一个字符)。如aad的前缀:a, aa
- 后缀:一个字符串的子串(去除第一个字符)。如dac的后缀:c, ac
- 最长相等(公共)前后缀:一个字符串里前后缀相等,最大的前缀或后缀的长度。例如:adada,a和a是相等前后缀,ada和ada也是。但是最长的那个是后者,因此是3,
注意!请思考:按照定义,特别地,字符串’a‘有前后缀吗?
3 难点
- 暴力匹配:我相信大家都会暴力匹配吧,怎么做呢?其实就是我们人工手动匹配的方法,只是我们的大脑快速运转而已。。例如,文本串:aadaadaaf 子串:aadaaf,当匹配到文本串的第二个d的时候,发现于子串不相同,这个时候,暴力就是直接把子串的第一个a再次与文本串的第二个a比照,然后遇到不相同的时候,再从子串的第一个开始比…其实大家都懂,那我就不赘述。
- KMP算法:kmp算法想要告诉我们的是:如果子串具有部分相同的前后缀,那么我们可以跳过某些部分的比较,它是怎么做的呢?还是上面那个例子:匹配到子串的f的时候,你会发现aadaa其实是由相等的前后缀aa的,这意味着什么呢?意味着我可以直接跳过子串前缀的比较,直接开始匹配子串前缀后面的部分这里理解了就好办!看下图:
在圈中的红框内,有最长相同前后缀2,也就是前后两个aa,试想一下,我第一次匹配前缀aa的时候,不就是同时也在进行后缀aa的匹配吗?(继续看下图)
按照kmp算法,我们下一步是如上图所示的。我们直接就把模式串的第一个d和文本串的第二个d比较了,注意:这个红色框位置没有变哦你发现没有,kmp算法没有比较1号位置的aa,为什么呢?就像第一张图片说的,我已经知道红色框部分是前后缀相同的了。也就是说:文本串中,在红框中的这五个字符组成的字符串必定也是前后缀相同的。也就是说:我们只要把模式串前缀这一坨(绿色波浪线)移动到与文本串中红色框的这一坨后缀(2号蓝色波浪线)重贴即可,并且不需要再匹配这一坨,因为我们已经知道这一坨就是相等的前缀和后缀!
- next数组:next数组是能够实现kmp算法的关键,它直接揭示了模式串中的前后缀相等情况。
4 代码解析
# 复杂度:O(m)
def get_next(s):
ls = len(s)
next_list = [0 for i in range(ls)]
j, i = 0, 1 # 定义前缀末尾,后缀末尾(只有前两个字符的字符串有这个概念!)
for i in range(1, ls): # 后缀末尾i是一直+1的
while(j > 0 and s[i] != s[j]): # 如果遇到不相等的字符:回退
j = next_list[j-1]
if s[i] == s[j]: # 如果遇到相等的字符:记录
j += 1
next_list[i] = j # 注意:j扮演两个角色:1. 以索引j为结尾的前缀的最大相同前后缀长度;2. 索引i处匹配不正确时应该回退的索引位置
# 3. 仔细想想索引i匹配不正确时候,不就是应该要回退到前缀的后一个字母吗?因为相同部分(相同前后缀)肯定相等啊(节约了时间!)
return next_list
def kmp(s, pattern):
if not pattern: return 0
ls, lp = len(s), len(pattern)
nex = get_next(pattern) # 获取next数组
j, i = 0, 0
while(i < ls):
# 如果不相等
if s[i] != pattern[j]:
if j == 0:
i += 1
else:
j = nex[j - 1]
else: # 如果相等
i += 1
j += 1
# 如果j是最后一个,则匹配成功
if j == lp: return i - lp
return -1