kmp算法超详细

在计算机科学中,字符串匹配是一个常见的问题。给定一个文本串和一个模式串,我们需要在文本串中找到所有与模式串匹配的位置。传统的字符串匹配算法如暴力匹配(Brute Force)方法在最坏情况下的时间复杂度为O(m*n),其中m和n分别是文本串(长的字符串)和模式串(短的字符串)的长度,kmp算法是一种高效的字符串匹配算法。

废话不多说我们直接介绍重点,带你理解kmp算法

1. kmp 算法原理 

为什么暴力匹配这么慢?

我们发现当每次匹配失败后,bf算法都会让 文本串(长的字符串)后退到匹配的第一个字符的下一个字符,让模式串(短的字符串)后退到第一个字符,重新开始匹配,例如:

0 1 2 3 4 5 6 7 8 9 10
a b c a b a b c a b d
a b c a b d
0 1 2 3 4 5
当我们使用bf算法时,在 5 下标位置匹配失败时,会让 文本串 后退到 1 下标 ,让模式串后退到 0 下标,重新开始匹配,但其实我们发现:文本串的 1 下标和 模式串的 0 下标其实并不匹配,其实大可跳过,文本串的 1 下标,如果重新匹配,我们发现,只有从文本串的 3 位置开始匹配才可能成功,kmp算法对于bf算法的优化就是在于,跳过了那些一定匹配不上的位置。

 kmp的算法核心在于,让文本串不后退:

如上述例子,我们在5位置匹配失败了,此处不让文本串后退,只让 模式串 后退,我们发现,文本串,的 3 4 下标是和模式串的 0 1 下标匹配的,所以我们可以让,让模式串后退到 2 下标位置,与 文本串 5 下标位置 进行比较:

0 1 2 3 4 5 6 7 8 9 10
a b c a b a b c a b d 
         a b c a b d
         0 1 2 3 4 5

即相当于,我们跳过了用 模式串 与 1 位置, 和 2 位置 的比较,因为这两个位置匹配一定是失败的,也跳过了 ,模式串 的 0 1 下标和,文本串的 3 4 下标的比较,因为我们知道一定是成功的,所以直接从模式串的 3 位置与文本串的 5 位置开始匹配

我们要如何知道 模式串 回退的位置?靠眼睛看肯定是不行的

如果上面的内容没有看懂,没关系,请重点理解下面的内容:

2. 最长前缀后缀

在模式串中,如果一个子串的前缀和后缀相同,则称该子串为前缀后缀。例如,模式串"a b c a b "的前缀有"a"、"ab"、"abc"、"abca"、"abca",后缀有"b"、"ab"、"cab"、"bcab"、"abca"。 
那么"abcab"的最长前缀后缀不就是 "ab"吗。

注意:最长前缀后缀不能是这个字串本身

练习一下:
"abcdbcabcd"的最长前后缀是?
没错,是 "abcd"。现在你已经会求最长前后缀了,现在我们可以解决,模式串回退的位置的问题了,这是最关键的一步。
以刚才的例子来说:

0 1 2 3 4 5 6 7 8 9 10
a b c a b a b c a b d
a b c a b d
0 1 2 3 4 5
现在我们发现了不匹配的地方,根据kmp算法,我们只回退模式串,要知道回退的位置,我们刚才的最长前缀后缀就有用处了。我们发现:前面绿色的代码匹配成功的字串。
我们现在把这两个字串的最长前缀后缀标出:

0 1 2 3 4 5 6 7 8 9 10
a b c a b a b c a b d
a b c a b d
0 1 2 3 4 5

现在你发现了什么。
没错,模式串中匹配成功的字串的 0 1 下标 和 3 4 下标互为最长前缀后缀,即 0 1 下标 的字符与 3 4 下标的字符相等,即模式串的的0 1 下标与 文本串中的 3 4 下标 相等,所以我们移动模式串,让模式串已经匹配成功的字串的 前缀 与 文本串中的后缀对应:

0 1 2 3 4 5 6 7 8 9 10
a b c a b a b c a b d
         a b c a b d
         0 1 2 3 4 5
于是我们得出,模式串匹配失败后的回退的位置 为 最长前后缀的 前缀 的后一个位置,也就是前缀/后缀的长度当前例子为 2 

如果理解了这一步,kmp 的关键你就已经掌握了。
接下来就是一个重复的过程了,模式串中每一个字符的前面的字串都有最长前缀后缀,而且最长相等前后缀的长度是我们移位的关键,所以我们用一个next数组记录下每个字符前面的字串的最长前缀后缀的长度,即在该字符匹配失败后模式串回退的位置。
例如:a b c a b d 的next数组为:
下标:          0 1 2 3 4 5 
模式串:       a b c a b d
next数组:   -1 0 0 0 1 2
注意:在第一个位置 为 -1 做特殊处理,下面会详细讲解。最长前后缀为空字符串即长度为0。

下面做一个练习:
abcababcabca的next数组为?

 

答案:

-1 0 0 0 1 2 1 2 3 4 5 3

3. 计算next 数组

 将next数组用代码计算出来:

我们发现,next 的值一定是序数递增的,不会由 0 直接到2,只能先到1 再到2。

举个例子:
 0 1 2 3 4 5
 a b c a b d
-1 0 0 0 1 2

5 位置前的字串 最长前缀后缀为 a b 长度为2,那 4 前面的字串的最长前缀后缀一定为 a 
如果4 前面 的最长前缀后缀为 0 即 0 位置的字符,与 3 位置的字符没有匹配上,  5 前面的字串的 最长前缀后缀 长度要为2 的话只能是 0 1小标和3 4 下标,所以0 3下标必须是匹配的 长度才可能为2。

那么,我们求 next数组,是不是就可以只判断当前的前一个字符是不是和他的最长前缀后缀的长度的对应位置的字符是否相等,如果相等,则当前的next值即为前一个值+1.

举个例子:

 0  1  2  3  4  5  6  7  8  9 10 11
 a  b  c  a  b  a  b  c  a  b   c   a
-1  0  0  0

现在要求4下标的next值,我们判断 4 下标的前一个字符 即 3下标的 a 是否和 它的 最长前缀后缀的长度 即对应的next值 即 0 位置的元素 即 a 是否相等 ,显然 a 与 a 相等,所以 4 下标的值应为 
0 + 1 = 1
 0  1  2  3  4  5  6  7  8  9 10 11
 a  b  c  a  b  a  b  c  a  b   c   a
-1  0  0  0  1    

同理,5下标 只需判断 4 下标 下的字符 是否等于 1 下标的字符,显然相等,所以 5 下标下的值应该为 1 + 1 = 2。

 0  1  2  3  4  5  6  7  8  9 10 11
 a  b  c  a  b  a  b  c  a  b   c   a
-1  0  0  0  1  2  

那如果比较的字符不相等呢?

 0  1  2  3  4  5  6  7  8  9 10 11
 a  b  c  a  b  a  b  c  a  b   c   a
-1  0  0  0  1  2  
1  2  3  4   5    
如现在求11下标的next值,我们比较 10 下标的 字符和 5 下标的字符 发现并不相等。
注意!!此时我们继续 判断 5 下标下的 next 的值 即 2 对应位置的字符是否和 10 下标下的字符相等,显然 c == c ,所以 10 下标下的 next 值即为 2 + 1 = 3.

以下为证明过程,理解不了直接记下结论即可

解释:因为我们尝试在 11 的前面找比 5 更长的最长前后缀,显然 匹配失败了,代表找不到,所以我们,继续向前,找“最长前后缀的前后缀的长度”,即在 abcab中找最长前缀后缀,即 5 下标下的next 值 2,由于 10 下标前的 最长前缀后缀的长度是知道的 ,即 5 ,说明 0 到 4 下标与 5 到 9 下标是匹配的,先找到 0 到 4 下标字串的最长前缀后缀 的长度为 ,2 那说明 0 1 下标 与 2 4 下标匹配,又因为 0 4 下标与 5 9 下标匹配 ,所以 ,0 1 下标 与 8 9 下标匹配,所以我们直接判断,2 下标与10 下标下的字符是否相等,相等则该下标对应的next 值应为 2 + 1 = 3. 如果不相等 则 继续 用 2 对应的 next 值往下找直到 为 -1 时做特殊处理

代码实现:

c语言版:

int* getNext(char* s, int len)
{
    //申请一块内存用于返回next数组
    int* next = (int*)malloc(4 * len);
    next[0] = -1; //处理特殊值,以便代码实现
    next[1] = 0; //第二个字符匹配失败一定是回到 0 位置开始比较
    int k = 0; // 代表前一个下标的 next 值,我们从2开始,所以k初始为next[1] = 0
    int i = 2;
    while (i < len)
    {
        //判断当前位置前一个字符是否等于 k 位置的 字符
        if (s[i - 1] == s[k])
        {
            //相等则当前位置的next值为 k + 1
            next[i] = k + 1;
            //让 i++
            i++;
            //让k的值更新
            k++;
        }
        else
        {
            //匹配失败,让当前字符与,next[k] 下标下的字符进行比较
            //我们这里直接更新k的值,在下一次循环比较,注意不需要 i++
            k = next[k];
        }
    }
    return next;
}

注意如果一直匹配失败会导致 k 的值 为 -1 导致 上面 数组越界
所以我们在上方的if中做特殊处理

int* getNext(char* s, int len)
{
    int* next = (int*)malloc(4 * len);
    next[0] = -1; //处理特殊值,以便代码实现
    next[1] = 0; //第二个字符匹配失败一定是回到 0 位置开始比较
    int k = 0; // 代表前一个下标的 next 值,我们从2开始,所以k初始为next[1] = 0
    int i = 2;
    while (i < len)
    {
        //判断当前位置前一个字符是否等于 k 位置的 字符,注意处理 k == -1
        if (k == -1 || s[i - 1] == s[k])
        {
            //相等则当前位置的next值为 k + 1
            next[i] = k + 1;
            //让 i++ 进入下次循环继续获取next数组
            i++;
            //让k的值更新
            k++;
        }
        else
        {
            //匹配失败,让当前字符与,next[k] 下标下的字符进行比较
            //我们这里直接更新k的值,在下一次循环比较,注意不需要 i++
            k = next[k];
        }
    }
    return next;
}

我们让k等于-1的时候也进入if内部,当k等于-1说明,当前i位置的字符前面最长前缀后缀的长度为0,此时我们给 next[i] 赋的值为 -1 + 1 = 0,所以这就是我们为什么要把next[0] 设置为 -1 

现在我们的 kmp 算法以及完成大半了,只需再写一个 函数用来比较两个字符串,在匹配失败的时候用next数组找到 模式串回退的位置,然后继续比较即可。

代码实现:

int KMP(char* s1, char* s2, int len1, int len2)
{
    int* next = getNext(s2, len2);
    int i = 0;
    int j = 0;
    while (i < len1 && j < len2)
    {
        //注意处理j为-1的情况
        if (j == -1 || s1[i] == s2[j]) {
            i++;
            j++;
        }
        else
        {
            j = next[j];
        }
    }

    if (j == len2)
    {
        //j == len2 说明匹配成功了,返回s1中匹配成功时,匹配的第一个字符的位置
        return i - j;
    }
    //匹配失败返回-1
    return -1;
}

至此我们的 kmp 算法就完成了,如果觉得对你有帮助的话,请给一个免费的赞,有什么问题也可以在下方讨论。

Java代码:
 

    //求next数组
    public static int[] getNext(String s) {
        int n = s.length();
        int[] next = new int[n];
        next[0] = -1;//处理特殊值,以便代码实现
        int i = 2;//第二个字符匹配失败一定是回到 0 位置开始比较所以直接从 2 位置开始
        int k = 0; // 代表前一个下标的 next 值,我们从2开始,所以k初始为next[1] = 0
        while (i < n) {
            //判断当前位置前一个字符是否等于 k 位置的 字符,注意处理 k == -1
            if(k == -1 || s.charAt(i-1) == s.charAt(k)) {
                //相等则当前位置的next值为 k + 1
                next[i] = k + 1;
                //让k的值更新
                k++;
                //让 i++ 进入下次循环继续获取next数组
                i++;
            }else {
                //匹配失败,让当前字符与,next[k] 下标下的字符进行比较
                //我们这里直接更新k的值,在下一次循环比较,注意不需要 i++
                k = next[k];
            }
        }
        return next;
    }

    public static int KMP(String s1, String s2) {
        int n = s1.length();
        int m = s2.length();
        int i = 0;
        int j = 0;
        int[] next = getNext(s2);

        while(i < n && j < m) {
            //注意处理 j == -1
            if(j == -1 || s1.charAt(i) == s2.charAt(j)) {
                i++;
                j++;
            }else{
                j = next[j];
            }
        }
        if(j == m) {
            return i - j;
        }
        return -1;
    }

  • 25
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 7
    评论
KMP算法是一种字符串匹配算法,它的全称是Knuth-Morris-Pratt算法,由Donald Knuth、Vaughan Pratt和James H. Morris三人于1977年联合发表。KMP算法的核心思想是利用已知信息尽可能地减少匹配的次数。 KMP算法的实现需要两个步骤:预处理和匹配。预处理阶段是为了计算出模式串中每个位置的最长公共前后缀长度,匹配阶段则是利用预处理结果进行匹配。 具体来说,预处理阶段需要计算出模式串中每个位置的最长公共前后缀长度,这个长度可以用一个数组next来存储。next[i]表示模式串中以i结尾的子串的最长公共前后缀长度。计算next数组的方法是从前往后依次计算,假设已经计算出了next到next[i-1],现在要计算next[i],则需要比较模式串中以i-1结尾的子串和以0结尾的子串、以1结尾的子串……以next[i-1]结尾的子串,找到最长的公共前后缀即可。 匹配阶段则是利用预处理结果进行匹配。具体来说,假设现在要在文本串中查找模式串,首先将模式串和文本串的第一个字符进行比较,如果相等,则继续比较下一个字符,否则需要根据next数组来移动模式串的位置。具体来说,假设当前模式串的位置是j,文本串的位置是i,如果模式串中第j个字符和文本串中第i个字符不相等,则需要将模式串向右移动j-next[j]个位置,这样可以保证模式串中前next[j]个字符和文本串中前i-(j-next[j])-1个字符是相等的。如果模式串已经移动到了最后一个字符,说明匹配成功,否则继续比较下一个字符。 KMP算法的时间复杂度是O(m+n),其中m和n分别是模式串和文本串的长度。KMP算法的优点是可以在O(m+n)的时间内完成匹配,而且不需要回溯文本串中已经匹配过的字符。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Ting-yu

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值