一文搞懂KMP算法

字符串匹配

假设原字符串长度是n,模式串(用于匹配的字符串)长度是m

朴素算法

如果按照最朴素的思维来实现字符串匹配算法,那么时间复杂度会是O(mn)的

我们来看最朴素的算法

for(int i = 1; i <= n; i++){
    boolen flag = true;
    for(int j = 1; j <= m; j++){
        if(p[j] !=  s[i + j - 1]){
            flag = false;
            break;
        }
    }
}

第一重循环是遍历原串s[N] 第二重循环遍历模式串p[M]

可能会朋友问,为什么是s[i + j - 1],因为在每次与模式串字符比较的时候,i指向的位置是不变的,为了找到原串的相应字符,需要用i加上一个偏移量

假设外层循环到第二次,此时i的值是2,内层循环j依旧从1开始

第一次是s[2]和p[1]比较,此时偏移量是1 - 1 = 0

在这里插入图片描述

第二次是s[3]和p[2]比较,此时偏移量是2 - 1 = 1

在这里插入图片描述

第三次是s[4]和p[3]比较,此时偏移量是3 - 1 = 2

在这里插入图片描述

可见这种朴素算法时间效率是非常低的,由此引出另一种算法,KMP算法

KMP算法

一个人能走多远不在于他在顺境时能走多快,而在于他在逆境时多久能找到曾经的自己。

上面的朴素算法中明显有很多不必要的比较,那么我们是否可以借助“某个力量”让我们规避掉这些不必要的比较

假设我现在有这样一个数组int[] next,这个数组记录的是当模式串字符和原串字符比较不相同时要回退的位置

我们先不纠结这个数组是如何创建的,我么先感受一下用上了这个数组会怎样

在这里插入图片描述

在kmp中数组下标都是从1开始的,这一点很重要,不然后续可能会看不明白

当我们比较到s[6]和p[5 + 1]不相等时,我们根据next数组的位置,将模式串的指针回退,回退到2的位置,此时再次比较s[6]和p[2 + 1]是否相等,如果相等就继续往后比较,如果不相等,就继续回退直到退到0为止

在这里插入图片描述

实现代码如下

for (int i = 1, j = 0; i <= n; i++) {
    while (j > 0 && p.charAt(j + 1) != s.charAt(i)) {
        //回退
        j = next[j];
    }
    if (p.charAt(j + 1) == s.charAt(i)) {
        //模式串比较成功的字符个数
        j++;
    }
    if (j == m) {
        //打印在主串中的位置
        System.out.print((i - m + 1) + " ");
        j = ne[j];
    }
}

可以看到整个算法的核心是next数组应该如何创建,next数组中存的是在该字符之前部分字符串中最大前缀和后缀相等的长度

比如下图中我们想要求得d对应的next值,我们就看d前面部分的字符串中前缀和后缀相等部分的长度,然后最大长度加一就是应该填入的值

在这里插入图片描述

到了这一步一定有朋友想搞明白这是什么原理,为什么要按照这种方式求next值

我们简单证明一下

在这里插入图片描述

假如这是13个数,我们正在求第13个数的next值

如果第12个数的next值是6,也就是说前11个字符中最大相等前后缀的长度是5,在图中表现的是蓝框中的部分是相等的

在这里插入图片描述

如果6和12字符相等,那么13的next值就是6 + 1 = 7

如果6和12字符不相等,那么我们就继续看6的next值,假如6的next值是3,那么这些红框中的字符串一定相等

在这里插入图片描述

如果3和12字符相等,那么13的next值就是3 + 1 = 4

如此操作一直重复下去

代码如下

// 计算 next 数组
int i = 1, j = 0;
while (i < n){
    if (j == 0 || p.charAt(i) == p.charAt(j)) {
        next[++i] = ++j;
    } else {
        j = next[j];
    }
}

可见KMP算法的时间复杂度是O(n+m)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值