KMP算法:求next数组,一听就会

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

再加上边界条件,就得到了完整的getNext方法:

func getNext(patten string) []int {
    next := make([]int, len(patten))
    if len(patten) == 0 {
        return next
    }
    next[0] = -1
    for i:= 1; i= 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

把这段优化加到上面去,得到最终的完整代码:

func getNext(patten string) []int {
    next := make([]int, len(patten))
    if len(patten) == 0 {
        return next
    }
    next[0] = -1
    for i:= 1; i= 0 && patten[i-1] != patten[k] {
            k = next[k]
        }
        next[i] = k+1
    }
    for i:=1; i

当然,这两个for循环可以合并成一个,但我认为合并之后反而不容易记忆。
希望你能从此记住kmp算法。反正我写完这一篇,大概忘不了了吧。

你可能感兴趣的:(KMP算法:求next数组,一听就会)