三分钟理解KMP算法

前言:一般的书籍或文章讲解KMP算法都是从问题分析、思路讲解再到代码讲解。但是我觉得先看懂代码、再捋清思路反而会更容易,所以本文会以代码+代码分析的方式来讲解KMP算法。

一、BF算法的代码

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

int BF(const char* txt, const char* pat)
{
    int i = 0;
    int j = 0;
    int txt_len = strlen(txt);
    int pat_len = strlen(pat);
    while (i < txt_len && j < pat_len)
    {
        if (txt[i] == pat[j])
        {
            i++;
            j++;
        }
        else
        {
            i = i - j + 1;
            j = 0;
        }
    }
    if (j == pat_len)
    {
        return i - j + 1;
    }
    return -1;
}

int main()
{
    char txt_str[101];
    char pat_str[11];
    puts("Please input main string (Max 100 chars):");
    gets(txt_str);
    puts("Please input pattern string (Max 10 chars):");
    gets(pat_str);
    printf("Matched position: %d\n", BF(txt_str, pat_str));
    return 0;
}

二、BF算法没有处理好的地方

当主串的第4个字符和模式串的第4个字符不匹配时:

  1. 主串:aaaaaaac,模式串:aaab
    BF算法:从主串的第2个字符和模式串的第1个字符开始向后匹配;
    KMP算法:从主串的第4个字符和模式串的第3个字符开始向后匹配。
  2. 主串:aaacaaab,模式串:aaab
    BF算法:从主串的第2个字符和模式串的第1个字符开始向后匹配;
    KMP算法:从主串的第4个字符和模式串的第3个字符开始向后匹配。

可以看到BF算法在字符不匹配时,下一次比对字符的位置会做没有意义的回退。

三、KMP算法的代码

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

void init_next(const char* pat, const int pat_len, int* next)
{
    next[0] = -1;
    int j = 0;
    int k = -1;
    while (j < pat_len - 1)
    {
        if (k == -1 || pat[j] == pat[k])
        {
            j++;
            k++;
            next[j] = k;
        }
        else
        {
            k = next[k];
        }
    }
}

int KMP(const char* txt, const char* pat)
{
    int i = 0;
    int j = 0;
    int txt_len = strlen(txt);
    int pat_len = strlen(pat);
    int* next = (int*)malloc(pat_len);
    init_next(pat, pat_len, next);
    while (i < txt_len && j < pat_len)
    {
        if (j == -1 || txt[i] == pat[j])
        {
            i++;
            j++;
        }
        else
        {
            j = next[j];
        }
    }
    free(next);
    next = NULL;
    if (j == pat_len)
    {
        return i - j + 1;
    }
    return -1;
}

int main()
{
    char txt_str[101];
    char pat_str[11];
    puts("Please input main string (Max 100 chars):");
    gets(txt_str);
    puts("Please input pattern string (Max 10 chars):");
    gets(pat_str);
    printf("Matched position: %d\n", KMP(txt_str, pat_str));
    return 0;
}

四、KMP算法的处理方式

  1. 设主串的长度为n,模式串的长度为m,则BF算法的时间复杂度是O(n*m),空间复杂度是O(1),KMP算法的时间复杂度O(m+n),空间复杂度O(m)。
  2. KMP算法分为两步,第一步是构造next数组(由init_next函数实现,时间复杂度为O(m),空间复杂度为O(m)),第二步是顺序逐个字符遍历主串(由KMP函数中的while语句实现,时间复杂度为O(n))。
  3. 模式串为aaab时,next数组为{-1, 0, 1, 2}。
  4. 变量i指示主串当前比对的索引位置,变量j指示模式串当前比对的索引位置,变量k指示模式串下一次比对的索引位置。
  5. next数组的值只与模式串有关,与主串无关,一共pat_len个元素,元素next[j]的值存储的是pat[j]与txt[i]不匹配时下一次与txt[i]比对的pat[k]的模式串索引位置k,k一定比j小。数组中-1和0代表重新从pat[0]开始比对,1代表重新从pat[1]开始比对,2代表重新从pat[2]开始比对,依此类推。
  6. next[0]一定等于-1,next[1]一定等于0,next[j]的值只与pat[0]到pat[j-1]这j个字符有关,next[j]的值与pat[j]是什么字符无关。
  7. 现在来讲讲next数组的值要怎么确定。字符串的前缀指除最后一个字符以外,字符串的所有头部子串。字符串的后缀指除第一个字符以外,字符串的所有尾部子串。以模式串aaab为例,next[0]和next[1]是固定值,next[2]的值由pat[0]到pat[1]这个字符串"aa"决定,"aa"的前缀为{a},后缀为{a}, { a } ∩ { a } = { a } \{a\}\cap\{a\}=\{a\} {a}{a}={a},{a}中最长的子串的长度为1,故next[2]等于1,next[3]的值由pat[0]到pat[2]这个字符串"aaa"决定,"aaa"的前缀为{a,aa},后缀为{a,aa}, { a , a a } ∩ { a , a a } = { a , a a } \{a,aa\}\cap\{a,aa\}=\{a,aa\} {a,aa}{a,aa}={a,aa},{a,aa}中最长的子串的长度为2,故next[2]等于2。
  8. 从代码实现的角度来说,在init_next函数中变量k存储的是当前子串(pat[0]到pat[j-1]这j个字符)的最长相等前后缀的前缀最后一个字符的索引位置,大家可以加一些打印自己跑一下代码。
  9. KMP函数中while循环里做的事情就很简单了。如果当前字符相同,则继续比对下一个字符,如果当前字符不同,则将主串中的当前字符继续与next数组指定的模式串中的字符比对。如果j到达模式串的结尾则退出循环(说明已经匹配到了结果),如果i到达主串的结尾了j依然没有到达模式串的结尾则说明主串中不存在模式串。

五、KMP算法的进一步优化

  1. 我们再来思考一种情况,当模式串为abab时:
    原来的KMP算法的next数组:{-1, 0, 0, 1}
    改进后的KMP算法的next数组:{-1, 0, -1, 0}
  2. 当txt[i]和模式串的第4个字符’b’不匹配时,老算法会回到模式串的第2个字符’b’开始比对(注意上面两个next数组的第4个元素的差异),改进后的算法会回到模式串的第1个字符’a’开始比对,因为txt[i]既然不等于模式串的第4个字符’b’,那么txt[i]也不会等于模式串的第2个字符’b’。
  3. 改进前后next数组的差异都与上面这种原因类似,许多资料也将改进后的next数组称为nextval数组。
  4. 将init_next函数里if语句中的next[j] = k;语句替换为如下代码即可:
    if (pat[j] != pat[k])
    {
        next[j] = k;
    }
    else
    {
        next[j] = next[k];
    }
    
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值