KMP小扩展,找出子串在主串中出现的所有位置

nurvnurv 2020-02-02

KMP算法能够高效地匹配字符串,找出子串(T串)在主串(S串)中出现的首个位置的原算法网上已经有很多优秀的博文进行详细讲解,这里就不多赘述。

这篇博文主要是对KMP原算法稍作改动,使其能够在主串中把所有匹配的主串找出来。


找出首个匹配的算法好弄,next数组求出来后直接用来匹配,直到出现完全匹配的情况的时候就停止搜索把答案扔出来就行,但是想把所有T串找出来的话就得完全把S串搜完, 就算已经在S串中找到一个T串后也是不能马上停止搜索的。

难点就在已经完全匹配了一个T串以后怎么继续进行下一个匹配。

完全匹配T串后,我们需要将S串的i指针往下挪一位,那么容易知道前面的字符串都已经是匹配过的了,根据KMP算法的思想,我们需要将T串的j指针进行回溯就能继续匹配,问题就在于这个j指针应该回溯到哪里才合适。

这里给出一个例子

主串(S串) ababaab

子串(T串) aba

这里的下标就从0开始吧,我们看到T串在S串中出现的位置是0和2,现在我们先把第一次匹配的情况画出来,这里next数组也顺便给出来了,这是没优化过的next数组

KMP小扩展,找出子串在主串中出现的所有位置

然后根据原KMP算法,i++, j++

KMP小扩展,找出子串在主串中出现的所有位置

按照原算法,在下个循环当中应该已经退出循环了,但是在这里我们当然不能这么做,所以我们应该将j回溯,不过这问题来了,之前我们进行回溯是根据next数组来将j进行回溯的,在上图的情况下,我们可以看到j所指的位置并没有对应的next值,那怎么办?

先不讨论算法,就单纯观察我们知道上图中的j应该是回溯到了b(T串1位置)那里,因为T串末尾的a跟首部的a相同,也就是这部分的后缀分前缀是相同的,根据KMP算法思想,这部分匹配过,就应该回溯到相同前缀的后一位,这就回溯到了b

到这里我们就已经是发现了,其实这种回溯就是在把next数组多往后求一位,虽然传统的KMP算法在求next数组时只是求出跟T串等长长度而已,但其实多往后求一位也是可以的,我们回到求T串求到最后一位时的情景:

KMP小扩展,找出子串在主串中出现的所有位置

指针的情况在如上图所示时,按传统算法本该退出循环的,但是理论上确实还是可以再往后求一位的,这里又有T[i] == T[j],所以i++, j++,然后next[i] = j,这样,我们就又额外得到了一位next数组的值

KMP小扩展,找出子串在主串中出现的所有位置

这个最后一位的next值意义是一样的,我们可以把这个字符串看成是长度为4的字符串,然后这个T[3]字符非常诡异,任何字符都不与它相等,也就是说,这个位置是必定匹配失败的,但是由于前3位字符都匹配成功了,所以这个回溯依然是合理的。

这样一来,S串中找出多个T串的算法就好弄了,这个T串我们看成是原先T串上多延长了一位,但是这个最后一位怎么匹配也不会匹配成功,所以在这个匹配算法中最多只会匹配到原T串长度,然后匹配到T串+1长度时就会回溯j指针,通过这种方法,我们就可以找出所有的T串出现位置了。


附代码(next数组优化过的那种):

vector<int> KMP(const string& S, const string& T)
{
    vector<int> Next;
    Next.push_back(-1);

    for (int i = 0, j = -1; i < T.size();) {
        if (j == -1 || T[i] == T[j]) {
            i++, j++;
            if (i != T.size() && T[j] == T[i]) Next.push_back(Next[j]);
            else Next.push_back(j);
        }
        else j = Next[j];
    }

    vector<int> res;
    for (int i = 0, j = 0; i < S.size() && j < (int)T.size();) {
        if (j == -1 || S[i] == T[j]) {
            i++, j++;
            if (j == T.size()) {
                res.push_back(i - j);
                j = Next[j];
            }
        }
        else j = Next[j];
    }

    return res;
}

相关推荐