KMP算法

KMP算法详解

1d276906ce94414dba5ebe742d7e1f25

前言

这两天巩固C基础的时候,遇到了这个KMP算法,回想之前学数据结构时就没完全搞懂它,这次下定决心要搞懂它

什么时KMP算法

网上博客的描述:

KMP算法又称看毛片算法,是用来进行字符匹配的,比如要检查一个字符串S里是否有字符串P,如果用暴力算法的话,也是可以解的,但是效率特别低,时间复杂度为O(m * n),而如果你用看毛片算法的话,时间复杂度为O(m + n)。

至于为什么叫看毛片算法(🤣),我猜是因为输入法的事儿,输入法在中文模式时,输入kmp,就会出来那三个字。

正经的解释:

KMP算法是一种改进的字符串匹配算法,是由D.E.Knuth,J.H.Morris和V.R.Pratt提出的,所以简称KMP算法。其算法核心是在匹配失败后利用next数组记录的信息来一定程度上减少匹配次数,以此提高字符串匹配的效率。

KMP算法的执行过程

我想,上来就将原理的话,估计也没多少人能彻底理解,所以,先看一下它的执行过程

一会儿用到的符号:

  • i ---> 被匹配字符串的下标
  • j ---> 匹配字符串的下标
  • next ---> next数组
  • next[j] ---> 下标j对应的值

执行过程

下标ij指示的字符相等,ij同时自增后移

image-20240505175245852

此时ij指示的字符依旧相等,ij继续自增后移,以此类推

image-20240505175500054

到第五个字符,也就是i=j=4时,我们发现ij指示的字符不相等,这时该怎么办呢?往后看

image-20240505175739077

  • 第一步,查看j指向的字符的下标[4]对应的next数组里的值next[4]=2
  • 第二步,令j指向下标为[2]的字符
  • 第三步,判断此时ij指向的字符是否一致,不一致,则类似的重复前两步,直到一致或next值为-1

将匹配字符串整体右移,保持ij对齐,如下图所示(实际程序执行中并没有这一步,这里只是为了方便理解)

image-20240505204256212

按照上面步骤调整好,我们就可以重新开始对比剩余字符是否相等

相等:ij自增后移,继续对比

不相等:重复上图所示步骤

终止条件,j>=匹配字符串的长度,这里也就是5

image-20240505181812804

KMP 算法原理

两大核心步骤:

  • 第一步,求解 next 数组
  • 第二步,进行字符串匹配

求解next数组

最长公共前后缀

在求解next之前,我们先来了解一下什么是 最长公共前后缀

字符串的前缀是指不包含最后一个字符的所有以第一个字符(索引为0)开头的连续子串

例如:字符串 "ABABA" 的前缀有:A,AB,ABA,ABAB

字符串的后缀是指不包含第一个字符的所有以最后一个字符结尾的连续子串

例如:字符串 "ABABA" 的后缀有:BABA,ABA,BA,A

公共前后缀:一个字符串的 所有前缀连续子串 和 所有后缀连续子串 中相等的子串

例如:

比如字符串 "ABABA"
前缀有:A,AB,ABA,ABAB
后缀有:BABA,ABA,BA,A
因此公共前后缀有:A ,ABA

最长公共前后缀:所有公共前后缀 的 长度最长的 那个子串

例如:

字符串 "ABABA" ,公共前后缀有:A ,ABA
由于 ABA 是 三个字符长度,A 是一个字符长度,那么最长公共前后缀就是 ABA

next数组

先来看一下next数组

字符串 A B A B D
下标 0 1 2 3 4
next -1 0 0 1 2

next数组的值的含义:

i=0时:

  • next[0]的值固定为-1,这既是规定,也是必须(后面的值在进行计算时会用到它)!表示后面匹配失败时,回溯的终止符(不一定回溯到next[0]=-1,但回溯到next[0]=-1就终止)

i>0时:

  • next[i]表示0~i-1个或前i个字符的最长公共前后缀

    例:next[4]表示前4个字符也就是ABAB的最长公共前后缀

  • next[i]也表示如果第i个字符匹配失败,接着i应该指向那个位置

    例:next[3]表示前3个字符字符匹配失败时,i因该指向1

  • next[i]还表示前0~i-1的字符都匹配,但下标为i的字符不匹配时,应该跳过几个字符的匹配

    例:next[2]表示前0~1的字符都匹配,但下标为2的字符不匹配时,应该跳过0个字符的匹配,也就是从下标0开始重新匹配

C语言实现代码

这个是比较好的实现代码:

void getNext(char *pattern, int *next) {
    int len = strlen(pattern);
    int j = 0, k = -1;
    next[0] = -1;
    while (j < len - 1) {
        if (k == -1 || pattern[j] == pattern[k]) {
            k++;
            j++;
            next[j] = k;
        } else {
            k = next[k];
        }
    }
}

这个是比较差一点的实现代码:

void makeNext(char *pattern, int *next) {
    int len = strlen(pattern);
    next[0] = -1;

    for (int i = 1; i < len; ++i) {
        int k = next[i - 1];
        while (k != -1 && pattern[i - 1] != pattern[k]) {
            k = next[k];
        }
        next[i] = ++k;
    }
}

进行字符串匹配

匹配过程就是上面的执行过程,实现代码如下:

int search(char *str, char *pattern, int *next) {
    int len1 = strlen(str);
    int len2 = strlen(pattern);
    int i = 0;
    int j = 0;

    while (i < len1 && j < len2) {
        if (j == -1 || str[i] == pattern[j]) {
            i++;
            j++;
        } else {
            j = next[j];
        }
    }
    if (j >= len2) {
        return i - len2;
    } else {
        return -1;
    }
}

全部代码

#include <stdio.h>
#include <string.h>

void makeNext(char *pattern, int *next);

void getNext(char *pattern, int *next);

int search(char *str, char *pattern, int *next);

int main() {
    char *str = "ABABABDABAABABAB";
    char *pattern="ABABDABAAB";
    int next[strlen(pattern)];

    getNext(pattern, next);
    int result = search(str, pattern, next);

    for (int i = 0; i < strlen(pattern); ++i) {
        printf("%d ", next[i]);
    }
    printf("\n");
    if(result != -1) {
        printf("匹配到相同字符串,下标从%d开始", result);
    } else{
        printf("未匹配到相同字符串");
    }
    return 0;
}

void makeNext(char *pattern, int *next) {
    int len = strlen(pattern);
    next[0] = -1;

    for (int i = 1; i < len; ++i) {
        int k = next[i - 1];
        while (k != -1 && pattern[i - 1] != pattern[k]) {
            k = next[k];
        }
        next[i] = ++k;
    }
}

void getNext(char *pattern, int *next) {
    int len = strlen(pattern);
    int j = 0, k = -1;
    next[0] = -1;
    while (j < len - 1) {
        if (k == -1 || pattern[j] == pattern[k]) {
            k++;
            j++;
            next[j] = k;
        } else {
            k = next[k];
        }
    }
}

int search(char *str, char *pattern, int *next) {
    int len1 = strlen(str);
    int len2 = strlen(pattern);
    int i = 0;
    int j = 0;

    while (i < len1 && j < len2) {
        if (j == -1 || str[i] == pattern[j]) {
            i++;
            j++;
        } else {
            j = next[j];
        }
    }
    if (j >= len2) {
        return i - len2;
    } else {
        return -1;
    }
}

结束语

说实话,我写的不够好,我推荐看这两篇文章:

这里另外附上洛谷的KMP算法题和AC代码:

image-20240507000835376

#include <stdio.h>
#include <string.h>

#define VALUE 1000000

void getNext(char *pattern, int *next);

int search(char *str, int i, char *pattern, int *next);

char str[VALUE];
char pattern[VALUE];
int next[VALUE];
int len1;
int len2;

int main() {
    scanf("%s", str);
    scanf("%s", pattern);

    len1 = strlen(str);
    len2 = strlen(pattern);

    getNext(pattern, next);
    int result = search(str, 0, pattern, next);

    while (result != -1) {
        printf("%d\n", result + 1);
        result = search(str, result + 1, pattern, next);
    }
    for (int i = 1; i <= len2; ++i) {
        printf("%d ", next[i]);
    }
    return 0;
}

void getNext(char *pattern, int *next) {
    int j = 0, k = -1;
    next[0] = -1;
    while (j < len2) {
        if (k == -1 || pattern[j] == pattern[k]) {
            k++;
            j++;
            next[j] = k;
        } else {
            k = next[k];
        }
    }
}

int search(char *str, int i, char *pattern, int *next) {
    int j = 0;

    while (i < len1 && j < len2) {
        if (j == -1 || str[i] == pattern[j]) {
            i++;
            j++;
        } else {
            j = next[j];
        }
    }
    if (j >= len2) {
        return i - len2;
    } else {
        return -1;
    }
}
posted @ 2024-05-07 00:21  星光辰枫  阅读(71)  评论(0)    收藏  举报