kmp 算法简介及 next 数组推导

Knuth-Morris-Pratt字符串查找算法(简称为KMP算法)可在一个主文本字符串S内查找一个词W的出现位置。此算法经过运用对这个词在不匹配时自己就包含足够的信息来肯定下一个匹配将在哪里开始的发现,从而避免从新检查先前匹配的字符。php

好比下面这种状况 html

gif中能够看出,匹配失败以后kmp算法不对主字符串的指针进行任何的回退,其关心的是对搜索词指针的处理。git

细心的你可能已经感觉到了一点,上面的处理是抽象的(通用的),既彻底不须要知道主字符串具体是多少的状况下进行的模拟演习。github

gif中模拟了指针k在字符c处失配的状况,经过这样一个预处理,在实际匹配中,若是遇到了这种状况,咱们只须要从容的将搜索词指针移动到E处,而后继续匹配便可。算法

补充一下,为何搜索词在移动到 K = E的位置停了下来? 这里能够感性的理解,因为在移动过程当中 AB成功进行了匹配,而在不知道 ‘?’所表明的具体字符是多少的状况下继续向前移动搜索词,则可能会出现错失匹配的状况。数组

如今依旧已ABEABC做为搜索词,再看几种演习状况学习

咱们来总结一下规律。 对于前两种状况K指针都没有移动到起点0,而是中途位置停了下来。 能够发现 ABEAB?与ABEABC在失配字符以前的字符ABEAB 的头部与尾部存在相同的字符AB测试

ABEA?和ABEAB在失配以前的字符ABEA中,头部和尾部存在相同的字符 Aspa

对于这种字符咱们称之为先后缀,经过上面的图咱们发现 K指针在失配时移动到的位置恰好是前缀的后一个字符。但一个字符串的前缀并非惟一的,因此这句话很是不严谨。

首先,要了解两个概念:"前缀"和"后缀"。 "前缀"指除了最后一个字符之外,一个字符串的所有头部组合;"后缀"指除了第一个字符之外,一个字符串的所有尾部组合。 3d

出自 阮一峰 字符串匹配的KMP算法

了解了前缀与后缀以后,咱们再次定义。 当指针j所指向的字符与指针k所指向的字符失配时,失配以前的字符存在一个前缀集合和一个后缀集合,咱们能够获得

k' = max(0 ~ k-1的前缀集合 ∩ 0 ~ k-1的后缀集合后缀集合)

k'的含义从公式中看的很清楚(前缀与后缀的交集的最大值)。而另外一层含义则是,若是搜索词下标从0开始计算,当k处失配时,咱们只须要将k移动到k'处继续匹配便可。

kmp算法的一般把计算出的k'放到next数组中存储 next[k] = k'。当咱们实战中在指针k处失配时,咱们只须要将k指针回退到k'处,既k = k' = next[k]便可。

确实如咱们以前所言,经过定义能够清楚的认识到,计算next数组彻底不须要主字符串参与,彻底是搜索词自匹配计算k' = max(0~k-1的前缀集合 ∩ 0~k-1的后缀集合)的过程。

这个定义虽然很严谨,便于理解,但却不能很好的使用计算机语言描述出来。下面看看便于计算机理解的next数组的推导过程。这应该是整个kmp算法最难理解的地方

next数组推导

根据next数组的定义,咱们能够有

next[j] = k,则 w[0 ~ k-1] = w[j-k ~ j-1]

要明白这二者之间是充分必要条件关系,既 若 w[0 ~ k-1] = w[j-k ~ j-1]next[j] = k

下图图中的状况为一种知足定义的状况next[6] = 2

这个我不知道怎么证实,由于这是由next数组的定义获得的,因此也不须要证实。

如今已经知道了next[j] = k,瓜熟蒂落,接下来咱们继续求next[j+1]next[j+1]求解过程当中存在两种状况

w[k] == w[j]

根据上面的推导,当 w[k] == w[j]时,有w[0 ~ k] = w[j-k ~ j], 则能够获得 next[j+1] = k + 1

w[k] != w[j]

w[k] != w[j]则进入了熟悉的字符串失配环节,明确一下,谁与谁的比较中产生了失配?下图是一个符合咱们讨论的例子

能够看出在寻找字符串ABEFABA的最大先后缀交集时,kj发生了失配

在kmp算法中若是发生了这种状况,则另 k = next[k],而后再次让w[k]与w[j]比较。那么问题来了

  1. 为何当w[k] != w[j]时,令 k = next[k], 而不是k = k-1或者其余呢?

    w[k]与w[j]失配时, k至少要移动到next[k]处才能使得k与主字符串的j继续匹配。这是next数组的定义,如今只不过在使用这个定义而已

  2. w[k] != w[j],因此另k' = next[k],假如此时w[k'] == w[j],如何证实 w[0 ~ k'] == w[j-k' ~ j] 呢?(图中粉色部分)

    k' = next[k]获得w[0 ~ k'-1] == w[k-k' ~ k-1]

    next[j] = k获得w[0 ~ k-1] == w[j-k ~ j-1]

    由于 w[0 ~ k-1] == w[j-k ~ j-1] 因此 w[k-k' ~ k-1] == w[j-k' ~ j-1]

    这里属于感性证实,能力不足暂时没法使用公式证实

    因此 w[0 ~ k'-1] == w[j-k' ~ j-1]

    又由于 w[k'] == w[j] 因此 w[0 ~ k'] == w[j-k' ~ j]

    w[0 ~ k'] == w[j-k' ~ j],获得 next[j+1] = k' + 1

    这是假如此时 w[k'] == w[j]的状况,但大多数状况是w[k'] != w[j]的,这种状况咱们在算法实现中讨论。

算法实现

next数组实现

private function getNext($word): array {
    $next = [-1];
    $len = strlen($word);
    $k = -1;
    $j = 0;

    while ($j < $len - 1) {
        if ($k == -1 || $word[$j] == $word[$k]) {
            $next[++$j] = ++$k;
        } else {
            $k = $next[$k];
        }
    }

    return $next;
}
复制代码

next[0] = -1 中-1是一种特殊标志,方便进行判断。在上面的w[k] != w[j]时,咱们另 k = next[k]而后再去判断w[k]是否等于w[j],若是仍是不相等,则再另k = next[k]像这样一直循环下去。 可是循环总归有个尽头,在尽头会出现这种状况,此时k = 0,w[k] != w[j],按照算法k = next[0] = -1

所以当咱们看到 k = -1时,咱们就可以知道 w[0 ~ k]不存在前缀与后缀的交集,既 max(0~k的前缀集合 ∩ 0~k的后缀集合) = 0 因此咱们另 next[k+1] = 0便可

上面的算法为了保持简洁性,令特殊值为-1,使得一个if,else能够覆盖三种状况,固然你用下面的写法也是一个意思

if($k == -1) {
    $next[++$j] = 0;
} elseif ($word[$j] == $word[$k]) {
    $next[++$j] = ++$k;
} else {
    $k = $next[$k];
}

复制代码

详细实现含测试用例 github.com/weiwenhao/a…

kmp算法实际上在字符串匹配中使用的状况并很少,虽然其时间复杂度是O(m+n),但实际上其表现跟朴素算法并不会差太多,在学习的过程当中其实也应该发现了,可以部分匹配的状况其实很少见。 不得不说kmp算法很是的难以理解,细节太多很容易陷入一个拆东墙补西墙的状况,各类牛角尖钻到停不下来。可是其状态机的思想,以及next数组的推导过程却很是值得学习。

相关文章
相关标签/搜索