字符串KMP算法及其优化的实现

在网上看了一堆别人写的,看了半天看不懂,来试试自己写一个,有不对的地方请指正


在介绍KMP算法之前首先讲一下BF算法(也就是普通的暴力破解),对于字符串匹配问题,我们一般是从主串第一个字符开始逐个和模式串进行匹配,如果不匹配则从主串第二个字符开始逐个进行匹配,以此类推,直到找到匹配的字符串

例如主串 “ABDAABC” 和模式串 “ABC” 比较,BF算法将从主串的每个字符开始都和模式串进行比较

方便起见将主串称为str,模式串称为pattern,用不同颜色代表不同字符,如下(第一轮比较):

在这里插入图片描述
首先str[0]pattern[0]进行比较,相同;然后str[1]pattern[1]比较,相同;然后str[2]pattern[2]进行比较,发现不相同

在这里插入图片描述
这时指针回溯到主串的str[1],开始重新和模式串进行比较,重复这个过程直到找到匹配的字符串,如下(第二轮比较):

在这里插入图片描述
对于BF算法,当匹配不成功时指向主串的指针就会进行回溯,在第一轮比较中发现str[2]pattern[2]不相等,指向主串的指针则从str[2]回溯到str[1],再次进行比较,由于从主串的每个字符开始都要和模式串进行一次遍历,因此BF算法的时间复杂度是 O ( n ∗ m ) O(n*m) O(nm),n为主串长度,m为模式串长度

回到正题,来解释KMP算法,在BF算法的基础上进行改进

如果在匹配失败不用回溯主串的指针,找到当前已匹配的主串和模式串的最长相同前后缀,模式串进行右移对齐,再比较当前指针所指向的位置是否相同,如果相同则继续比较下一位,如果不同则再找当前已匹配的主串和模式串的最长相同前后缀……重复上述的过程直到找到匹配的字符串

举例说明:

在这里插入图片描述
当前str[3]pattern[3]出现不匹配,但是当前指针之前都是已经匹配过的,现在只要找到已经匹配的主串和模式串(框里的部分)中的最长相同前后缀就不用回溯主串的指针

现在已经匹配的主串和模式串为"ABA",最长相同前后缀为"A",然后模式字符串右移对齐

在这里插入图片描述
对齐后再比较当前指针所指向的字符,不相等,则再找已经匹配的主串和模式串(框里的部分)的最长相同前后缀,由于"A"没有最长相同前后缀,所以模式串右移,再比较当前指针所指向的字符,不相等,然后主串指针向后移一位,继续比较

在这里插入图片描述
在这里插入图片描述


为了能够让主串知道在匹配失败时再和模式串中的哪个字符开始比较,我们设计一个数组用来存储在主串和模式串匹配失败时下一个与主串比较的模式串字符的位置,我们把这个数组称为next数组

在这里插入图片描述

我们先来解释next数组的意义,再解释这些值是怎么算的,next[j]表示当模式串在索引为j时匹配失败时,下一个与主串比较的模式串字符的索引

例如上面的例子,"ABACABAB"和"ABAB"比较,在str[3]pattern[3]时匹配失败

然后str[3]与模式串索引为next[3]的字符继续比较,也就是和pattern[1]比较,不相等;

然后str[3]与模式串索引为next[1]的字符继续比较,也就是和pattern[0]比较,不相等;

然后str[4]pattern[0]比较……

上面讲到的最长相同前后缀,其实next数组就是用来记录模式串子串的最长相同前后缀的长度,从另一个角度来看,next[j]也表示长度为j的子串的最长相同前后缀长度

如模式串"ABAB",子串有:"",“A”,“AB”,“ABA”

""		next[0]的值必然是-1				  //后面会讲为什么必然是-1
"A"		没有最长相同前后缀,所以next[1]=0   //因为只有一个字符,所以也必然是0
"AB"	没有最长相同前后缀,所以next[2]=0
"ABA"	最长相同前后缀为"A",所以next[3]=1

我们现在设变量ij分别是指向主串字符和模式串字符的索引

  1. 当主串和模式串匹配成功时,指向主串和模式串的索引都会指向下一个字符,即i++,j++
  2. 当主串和模式串匹配失败时,则要更新指向模式串的索引,即j=next[j]
  3. 考虑特殊情况,当主串和模式串的第一个字符匹配失败时,则指向主串的索引直接指向下一个字符,和模式串第一个字符继续比较,如果这里把next[0]的值给写成-1,这样也可以用i++,j++来表示对这种情况的处理

现在我们来分析怎么求模式串子串的最长相同前后缀

假设我们要求长度为i的子串最长相同前后缀,如果我们已经知道之前的子串的最长相同前后缀,就能求出当前子串的最长相同前后缀

也就是要求next[i]的值,设j为上一个子串的最长相同前后缀的长度,思路如下:

  1. 比较pattern[i-1]pattern[j](若j为-1则直接执行第二步)
  2. 若相等则令next[i]=j+1,若不相等则令j=next[j],重新执行第一步

举个例子,如果我们的模式串是"AAAAB",那么子串有 “”,“A”,“AA”,“AAA”,“AAAA”,要求这些子串的最长相同前后缀

在这里插入图片描述

若当前i为3,也就是求长度为3的子串的最长相同前后缀,上一个子串的最长相同前后缀为j=next[2]

所以将pattern[2]pattern[j]比较,也就是比较pattern[2]pattern[1]

相等,则next[3]等于next[2]+1


代码实现(C语言)

  1. BF算法

    /* str表示主串,pattern表示模式串 */
    int brute_force(char *str, char *pattern)
    {
        int i = 0, j = 0;
        while (i < strlen(str) && j < strlen(pattern))
        {
            if (str[i] == pattern[j])
                i++, j++;
            else
                i = i - j + 1, j = 0;
        }
        //返回字符索引,若没有则返回-1
        return j == strlen(pattern) ? i - j : -1;
    }
    
  2. KMP算法

    /* str表示主串,pattern表示模式串 */
    int kmp(char *str, char *pattern)
    {
        int i = 0, j = 0;
        int next[strlen(pattern)];
        //获取next数组
        get_next(pattern, next);
        //获取next_val数组
        //get_next_val(pattern, next);
    
        // 打印next数组
        for (int i = 0; i < strlen(pattern); i++)
        {
            printf("%d ", next[i]);
        }
        putchar('\n');
    
        while (i < (int)strlen(str) && j < (int)strlen(pattern))
            if (j == -1 || str[i] == pattern[j])
                i++, j++;
            else
                j = next[j];
        return j == strlen(pattern) ? i - j : -1;
    }
    
    /* pattern表示模式串 */
    void get_next(char *pattern, int next[])
    {
        //记录上一个子串的最长相同前后缀
        int j = -1;
        next[0] = -1;
        //i表示当前子串长度
        for (int i = 1; i < strlen(pattern);)
        {
            if (j == -1 || pattern[i - 1] == pattern[j])
                next[i++] = ++j;
            else
                j = next[j];
        }
    }
    

KMP算法的优化

考虑一种情况,当主串为"AAAACAAAAB",模式串为"AAAAB"时,此时next数组为:{ -1 , 0 , 1 , 2 , 3 }

当主串匹配到字符’C’时,此时i为4,j为4:

在这里插入图片描述
str[4]不等于pattern[4],所以str[4]pattern[next[4]]比较,也就是和pattern[3]比较

在这里插入图片描述
str[4]不等于pattern[3],所以str[4]pattern[next[3]]比较,也就是和pattern[2]比较

重复这样的流程,直到str[4]pattern[next[1]]比较,也就是和pattern[0]比较

在这里插入图片描述
在这里插入图片描述

这时我们发现这些比较都是多余的,在str[4]pattern[4]比较的时候,这两个字符不相等,而pattern[4]之前的字符和pattern[4]相等,所以也就没必要将pattern[4]之前的字符依次和str[4]比较了

所以在上面实现的KMP算法中,我们重新计算next数组的值,计为next_val数组,next_val数组比next数组更加的“智能”,在遇到这种情况知道跳过去,不用一个一个的比较

当主串和模式串比较时,str[i]pattern[j]比较失败时,需要让主串的字符继续与pattern[next[j]]比较,但是如果pattern[next[j]]pattern[j]相等,则依然是匹配失败的

在这里插入图片描述
所以在计算next_val数组的值时,我们增加一个判断条件,如果当前子串的后一位字符等于当前子串的最长相同前后缀的后一位字符,则令当前子串的next_val值为当前最长相同前后缀长度的next_val的值

/* pattern表示模式串 */
void get_next_val(char *pattern, int next[])
{
    //记录上一个子串的最长相同前后缀
    int j = -1;
    next[0] = -1;
    //i表示当前子串长度
    for (int i = 1; i < strlen(pattern);)
    {
        if (j == -1 || pattern[i - 1] == pattern[j])
            if (pattern[i] == pattern[j + 1])
                next[i++] = next[++j];
            else
                next[i++] = ++j;
        else
            j = next[j];
    }
}
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值