KMP算法是啥?
KMP算法就是一种字符串匹配算法,简单说就是从一个长字符串中搜索一个短字符串(也叫模式串)。
这个算法我从大学上数据结构课第一次学到,到现在反反复复学过不下十次,每次都当时觉得懂了,可就是记不住,相信很多人也有同感。最记不住的地方就是next数组的求法。
今天我尝试着用尽量容易理解的方法去讲解这个问题,希望我自己和各位读者从此把这个算法牢牢记住。
算法概述
前面的引子就不说了,KMP算法的关键在于next数组。next数组的作用是:匹配到某一位失败时,回退到next[i]这一位重新去匹配。
我定义了一个名词:端长
我这里自己定义一个名词“端长”,指的是字符串前缀和后缀相等的长度。比如字符串"ababa"的端长有3,1。因为前三位aba与后三位aba相等,长度为3,所以端长为3。第一位与最后一位也相同,所以1也是一个端长,但最大端长是3。
请记住这个“端长”的含义。我尝试过很多次去解释kmp算法的实现,发现这里必须有一个定义,才便于后面的描述和理解。
实际上,求next[i]的值就是求模式串第i个字符之前(不包括第i个)的子串的最大端长。
例如:模式串为ababacd,next[5]就是ababa这个字符串的最大端长,也就是3,所以next[5] = 3。
手动构造next数组
手动构造next数组的方法:前两位固定为-1和0,第三位,下标i=2,next[i] 的值为模式串p的前2位子串的最大端长,以此类推。对于手动构造,最大端长的值可以一眼看出来。
代码求next数组
问题在于,对于一个字符串,最大端长的值用代码怎么求?暴力法当然可以,但太慢了,每一次都需要O(n)的时间复杂度,而我们现在需要求len(p)个字串的最大端长,这样的复杂度太高了。
关键点在这里:我们可以用递推的方法来求最大端长。求next数组的时候,我们是从0开始从前往后算的。对于next[i]的值,我们可以利用next[i-1]的值来计算。
递推求端长
要确定这样一个前提:如果一个字符串p的最大端长为k,那么这个字符串后面再加一位之后,最大端长最多只能是k+1,这是上限。而且要等于k+1还有个前提,就是加的这一个字符等于p[k]。
例如:字符串ababa的最大端长为3,如果在后面加一个b,ababab,则最大端长变为4,因为p[3]是b。
下面开始递推:假如next[i-1]的值为k,也就是模式串p的0到i-2位的最大端长为k,那么如果p[k] == p[i-1],则可以得到结果:p的0到i-1为的最大端长为k+1,next[i] = k+1。
那如果p[k] != p[i-1]呢?可以继续寻找小一点的端长,这恰好是0到k-1字串的最大端长,把这个端长设为新的k,重复上面的步骤。
例如:如果要求ababaa的最大端长,先看去掉最后一位之后 ababa的最大端长是3,对应前缀是aba。但最后一位p[5]是a, 而p[3]是b,不相等。继续寻找ababa的下一个端长,其实也就是aba的最大端长1,正好p[1]是a,等于最后一位p[5],所以ababaa的最大端长就是1。
这部分逻辑转化成Go语言代码就是这样:
for i:= 1; i<len(patten); i++ {
k := next[i-1]
for patten[i-1] != patten[k] {
// 查找小一点的端长
k = next[k]
}
next[i] = k+1
}
再加上边界条件,就得到了完整的getNext方法:
func getNext(patten string) []int {
next := make([]int, len(patten))
if len(patten) == 0 {
return next
}
next[0] = -1
for i:= 1; i<len(patten); i++ {
k := next[i-1]
for k >= 0 && patten[i-1] != patten[k] {
k = next[k]
}
next[i] = k+1
}
return next
}
进一步优化
这样可以得到正确的结果,但还不够好。例如:用上面的算法,如果模式串是"aaaaaa",得到的next数组应该是[-1,0,1,2,3]。但仔细想想,如果第4个a匹配失败,还需要跳到第3个a重新匹配吗?肯定不需要,因为p[3]和p[4]都是"a",既然p[4]匹配失败,p[3]也一定匹配失败。因此我们得出一个优化方案:如果跳转后的字符和原字符相同,则继续跳转。转化为代码就是:
for i:=1; i<len(patten); i++ {
if patten[i] == patten[next[i]] {
next[i] = next[next[i]]
}
}
把这段优化加到上面去,得到最终的完整代码:
func getNext(patten string) []int {
next := make([]int, len(patten))
if len(patten) == 0 {
return next
}
next[0] = -1
for i:= 1; i<len(patten); i++ {
k := next[i-1]
for k >= 0 && patten[i-1] != patten[k] {
k = next[k]
}
next[i] = k+1
}
for i:=1; i<len(patten); i++ {
if patten[i] == patten[next[i]] {
next[i] = next[next[i]]
}
}
return next
}
当然,这两个for循环可以合并成一个,但我认为合并之后反而不容易记忆。
希望你能从此记住kmp算法。反正我写完这一篇,大概忘不了了吧。
网友评论