什么是KMP算法

KMP算法是由 D.E.Knuth、J.H.Morris 和 V.R.Pratt 提出的形式匹配算法,简称 KMP 算法。KMP 算法对 BF算法进行了很大改善,提高了匹配效率。

KMP算法和BF算法的详细区别这儿就不多做解说,首要解说怎么完成KMP算法

KMP算法剖析

有一个子串 ababc,咱们需求在主串 xxxxxxxxxxxx 中搜索这个子串中搜索这形式串。用 i 和 j 别离表明当时匹配到主串和子串的哪个方位

一文搞懂KMP算法

字符匹配成功:

当时字符匹配成功时,i 和 j 都 +1,匹配下一个字符

字符匹配失利:

当最终一个字符匹配失利时,那么阐明前面的字符都匹配成功,能够确认主串中前四个字符与子串的前四个字符持平。

一文搞懂KMP算法

假如依照BF算法,那么 i 需求指向 主串的第二个字符,j 指向子串的榜首个字符,然后重新匹配

一文搞懂KMP算法

每次产生匹配失利时 i 都需求往回退

而 KMP 算法 的 i 则不需求变化

为了方便观察,咱们能够在当时字符的左面画一条分界线,分界线左面的字符都是已知的

一文搞懂KMP算法

咱们能够让子串每次往左移动一个字符,看看子串的榜首个字符与主串的第二个字符是否持平

一文搞懂KMP算法

显然它们不持平,那么就没有必要在匹配一次这两个字符。再把子串往右移动一个字符

一文搞懂KMP算法

主串的第三个字符是 a,子串的榜首个字符也是 a,它们两个持平,那么比照子串的下一个字符和主串的下一个字符是否持平,显然也持平。主串下一个字符是不知道的,所以不能再移动子串。此刻 i 指向的是这个不知道的字符,而 j 指向的就是要与这个不知道字符匹配的子串字符,即第三个字符(j 之前的字符现已匹配成功了,不需求在进行匹配)。

所以当 第五个字符匹配失利时,咱们不需求让 i 往回退,让 i 坚持不变,j = 3 即可。

当第四个字符匹配失利时,阐明前面三个字符匹配成功,主串与子串的前三个字符都持平

一文搞懂KMP算法

在第四个字符前画一条分界线,分界线左面的字符已知

一文搞懂KMP算法

子串依次往右移动一个字符,比照子串的榜首个字符与主串的第二个字符是否持平,不持平时继续移动子串,比照主串的下一个字符。比照到主串的第三个字符时,与子串的榜首个字符持平,且下一个字符是不知道的字符,中止移动子串。

一文搞懂KMP算法

此刻 i = 4,j = 2。所以当第三个字符匹配失利时,直接让 j = 2 即可

当第三个字符匹配失利时,阐明前面两个字符匹配成功,主串与子串的前两个字符都持平

一文搞懂KMP算法

同样的在第三个字符前画一条分界线,分界线左面的字符已知

一文搞懂KMP算法

子串往右移动进行比照,a 与 b 不持平,继续移动一个字符

一文搞懂KMP算法

此刻子串来到了 i 这个方位,因为 i 所指向的主串的字符不知道,所以要和子串的榜首个字符进行匹配.此刻 i = 3,j = 1

最终剩下榜首和第二个字符,这两个方位有点特别

不管什么情况下,当榜首个字符匹配失利时,让 j = 0;当第二个字符匹配失利时,让 j = 1;

当第二个字符匹配失利时,主串的第二个字符是不知道的,因为咱们只知道它不等于 b,或许等于其他任何值。所以子串需求往右移动一位,和这个不知道的字符进行匹配

一文搞懂KMP算法

能够考虑一下,不论是什么情况下,只需第二个字符没有匹配上,就能够让 j = 1

当榜首个字符匹配失利时,直接从主串的下一位字符开端匹配。 依据前面的规律,i 需求坚持不变,j 需求等于某个值。为了坚持跟之前的处理逻辑相同,能够先让 j = 0,然后 i 和 j 都 +1

整合以上各个字符匹配过错时对应的 j 的值,能够得到一个 next数组

为了和子串字符的位序坚持一致,不运用 0 这个方位,直接从 1 开端表明

next[0] next[1] next[2] next[3] next[4] next[5]
0 1 1 2 3

在主串中搜索子串的方位时,当某一个字符匹配过错,能够让 j 等于 next 数组中这个字符所对应的值。这样能够越过一些不必要的匹配

用 next 数组来模仿搜索一下子串

主串:a c a b a d a b a b c

子串:a b a b c

一文搞懂KMP算法

榜首位匹配成功,i 和 j 都 +1

一文搞懂KMP算法

第二位匹配失利,依据 next 数组,让 j = 1

一文搞懂KMP算法

榜首位匹配失利,j 置为 0,i 和 j 都 +1

一文搞懂KMP算法

若匹配成功 i 和 j 都 +1,匹配下一个字符,直到遇到匹配失利的字符

一文搞懂KMP算法

第四个字符配对失利,i 坚持不变,j = 2

一文搞懂KMP算法

第二个字符配对失利,i 不变,j = 1

一文搞懂KMP算法

第二个字符配对失利,i 不变,j = 1

一文搞懂KMP算法

后面的字符都能配对成功,假如 j 大于了子串的长度,阐明主串中能够找到子串

一文搞懂KMP算法

代码完成

界说一个结构体,包含一个存放字符串的字符数组和一个变量记载字符串的长度

typedef struct {
	char ch[255];
	int length;
} SString;

准备主串、子串和一个空的 next 数组

字符串从 1 开端,所以 0 的方位能够用一个特别字符表明这个方位为空

next 数组也是从 1 开端存储,所以长度为 S2.length+1

int main() {
    SString S1 = { "#acabadababc", 11 };
    SString S2 = { "#ababc", 5 };
    int next[S2.length+1];
}

完成 next 数组

void nextArr(SString S1, SString S2, int next[]) {
    //  第 o 位用 -1 表明这个方位不运用
    next[0] = -1;
    //  不管什么情况榜首位都为 0
    next[1] = 0;
    //  不管什么情况第二位都为 1
    next[2] = 1;
    //  i 表明第几个字符匹配失利,循环的次数为子串的长度-3
    for (int i = 3; i <= S2.length; i++) {
        //  x 表明每次产生匹配过错,对主串前面已知的字符与子子串比照时主串开端的方位
        //  y 表明表明子串当时比照到的方位
        //  子串和主串的字符配对成功后,x 和 y 都需求 +1,然后比照下一个字符,若主串下一个字符与子串不持平,则需求让主串开端比照的方位往右移一位,所以需求用 z 来记载主串最初开端比照的方位
        int x = 2, y = 1, z = 2;
        //  循环比照主串已知的字符和子串是否相同。x 超越匹配失利的那个字符时中止循环
        while (x < i) {
            //  假如相同,x 好 y 一起 +1
            if (S1.ch[x] == S2.ch[y]) {
                x++;
                y++;
            //  假如不相同,主串开端比照的方位 +1,让 x 回到这个方位,y 回到子串的榜首位
            } else {
                z++;
                x = z;
                y = 1;
            }
        }
        //  比照结束时 y 的值就是子串下一个要匹配的字符的方位
        next[i] = y;
    }
}

把 next 传入搜索函数,依据数组来判别匹配失利时 j 的值应该为多少

int main() {
    SString S1 = { "#acabadababc", 11 };
    SString S2 = { "#ababc", 5 };
    int next[S2.length+1];
    //  把主串、子串和 next 数组传入 nextArr 函数
    nextArr(S1, S2, next);
    //  把主串、子串和 next 数组传入 search 函数
    int result = search(S1, S2, next);
    printf("result: %d\n", result);
}

完成 search 函数

int search(SString S1, SString S2, int next[]) {
    //  i 和 j 表明主串和子串匹配到了哪个字符
    int i = 1, j = 1;
    //  当 i 大于子串的长度或 j 大于子串的长度结束循环
    while (i <= S1.length && j <= S2.length) {
        //  假如这两个字符持平,则匹配下一个字符。因为当榜首个字符匹配失利时,会让 j = 0,然后 i 和 j 都 +1,所以这儿要处理 j = 0 的情况
        if (j == 0 || S1.ch[i] == S2.ch[j]) {
            i++;
            j++;
        } else {
            //  不持平则直接依据 next 设置 j 的值
            j = next[j];
        }
    }
    //  j 大于子串的长度阐明子串匹配成功,返回子串的方位,也就是子串在主串中榜首个字符的方位
    if (j > S2.length) {
        return i - S2.length;
    } else {
        //  匹配失利则返回 0
        return 0;
    }
}

最终运转代码看一下成果是否正确

一文搞懂KMP算法

测验另一个主串和子串

S1 = { “#eabcbcaeeab”, 11 }; S2 = { “#bcaeea”, 6 };

一文搞懂KMP算法

优化 next 数组

咱们有没有发现在上面的字符串中,第三位字符假如匹配失利,依据 next 数组,j 的值应该为 1,也就是说 j 应该指向子串的榜首个字符。可是子串的榜首个字符和第三个字符持平,都是 a,那么第三个字符匹配失利了,阐明主串中这个不知道的字符肯定不是 a,榜首个字符也肯定会匹配失利。所以咱们能够优化一下 next 数组,假如 S2.ch[j] == S2.ch[next[j]] 的话,能够直接让 j 的值等于榜首个字符 j 的值,这样能够越过这次肯定会失利的比照。

基于 next 优化成 nextval 数组

int main() {
    SString S1 = { "#acabadababc", 11 };
    SString S2 = { "#ababc", 5 };
    int next[S2.length+1];
    nextArr(S1, S2, next);
    int nextval[S2.length+1];
    nextvalArr(S2, next, nextval);
    int result = search(S1, S2, nextval);
    printf("result: %d\n", result);
}

完成 nextval 数组

void nextvalArr(SString S2, int next[], int nextval[]) {
    nextval[0] = -1;
    nextval[1] = 0;
    for (int j = 2; j <= S2.length; j++) {
        if (S2.ch[j] == S2.ch[next[j]]) {
            nextval[j] = nextval[next[j]];
        } else {
            nextval[j] = next[j];
        }
    } 
}

运转优化后的代码,成果也没有问题