(Java代码实现)最快的字符串匹配算法:KMP算法,详细刨析让你如庖丁解牛般掌握KMP算法

KMP算法是一种高效的字符串匹配算法,通过next数组避免不必要的字符比较,提高匹配速度。文章详细解释了KMP算法的核心思想,子串下标的移动规则,以及如何创建next数组。在匹配失败时,j会移动到适当位置,而不是每次都回溯到0,减少了比较次数。最后给出了KMP算法的代码实现。
摘要由CSDN通过智能技术生成


前言

本人是一个刚刚上路的IT新兵!分享一点自己的见解,如果有错误的地方欢迎各位大佬莅临指导,如果这篇文章可以帮助到你,劳请大家点赞转发支持一下!


提示:本篇文章,文字较多,还请各位细细咀嚼

一、KMP算法是什么?

KMP算法是一种改进的字符串匹配算法,由D.E.Knuth,J.H.Morris和V.R.Pratt提出的,因此人们称它为克努特—莫里斯—普拉特操作(简称KMP算法)。KMP算法的核心是利用匹配失败后的信息,尽量减少模式串与主串的匹配次数以达到快速匹配的目的。具体实现就是通过一个next()函数实现,函数本身包含了模式串的局部匹配信息。KMP算法的时间复杂度O(m+n)。

🤑🤑🤑那么就通过举例来讲解一下。
KMP算法是高效的字符串匹配算法
在这里插入图片描述

在主串中寻找这个字串的第一个下标


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

在这里插入图片描述

在这里插入图片描述

所以KMP算法的时间效率更高。


为啥KMP会将 j 移动到 3 位置呢🧐🧐

原因就是:

此时, i 走到了 7 位置, j 走到了 7 位置,就代表

主串中的[0,6]位置字符

子串中的[0,6]位置字符
一定相同。

[x,y]下标 表示x,x+1,x+2,...y这些下标

子串[0,2]下标的字符与子串[4,6]下标的字符相同,
主串[4,6]下标的字符与子串[4,6]下标的字符相同,

所以子串[0,2]下标字符与主串[4,6]下标字符相同。

所以 i 不需要动位置,把 j 挪动到3下标
此时子串[0,2]下标字符与主串[4,6]下标字符相同,无需重复进行比较。可以直接从3下标开始比较。


👀👀下面的推导是重点😇😇

推导至一般化

  • 无论主串中 i 在什么位置,
    如果子串中的 [ 0 , x - 1 ] 位置的字符串 (必须以0下标开头)

    子串中的[ j - x , j - 1] 位置的字符串 (必须以j-1下标结尾)
    相同;
    那么在 j 位置匹配失败时,就将 j 移动到x位置。(x为两个相同字符串的长度)

KMP算法的核心思想:通过上述推导的思想,不让主串下标 i 再移动位置了,当子串下标 j 位置匹配失败时,重新将 j 移动到合适的位置,再继续匹配


二、子串下标的移动

  • 移动方法:在下标 [ 0 , j - 1] 范围中寻找一个以 0 下标为开头,一个以 j - 1
    下标为结尾的,两个相等的字符串,他们的长度就是在 j 下标匹配失败后,子串下标 j 要移动的位置。

举例:

A.

背景:在主串的 X 下标处与子串中的 Y 下标处字符匹配失败,且 XY 均合法, X >= Y
在这里插入图片描述

假设在 7 下标的字符匹配失败。
所以要在子串中找到两个相同的以 0 下标 a 字符开头,以 7 - 1 下标 c 为结尾的两个相同的子串。(相同,但不能是同一个子串)

在这里插入图片描述
如图,下标[ 0 , 2 ] 与下标[ 4 , 5 ] 两个字符串相同。
所以当在 7 下标位置匹配失败时,应 主串下标 i 不变,子串下标 j 移至 3 位置然后继续匹配。


B.

在这里插入图片描述
假设在 5 下标的字符匹配失败。
所以要在子串中找到两个相同的以 0 下标 a 字符开头,以 5 - 1 下标 a 为结尾的两个相同的子串。(相同,但不能是同一个子串)

在这里插入图片描述

如图,下标[ 0 , 0 ] 与下标[ 4 , 4 ] 两个字符串相同。
所以当在 5 下标位置匹配失败时,应 主串下标 i 不变,子串下标 j 移至 1 位置然后继续匹配。


C.

在这里插入图片描述
假设在 4 下标的字符匹配失败。
所以要在子串中找到两个相同的以 0 下标 a 字符开头,以 4 - 1 下标 a 为结尾的两个相同的子串。(相同,但不能是同一个子串)

在这里插入图片描述
如图,下标[ 0 , 2 ] 与下标[ 1 , 3 ] 两个字符串相同。
所以当在 4 下标位置匹配失败时,应 主串下标 i 不变,子串下标 j 移至 3 位置然后继续匹配。


在这里我们引入两个变量;

int k;//代表子串中以0下标开头的字符串的结尾
int j;//代表子串中以j-1下标结尾的字符串的结尾

🤪🤪🤪重点重点重点!!!!

无论A,B,C哪种情况,
k != -1 时,k 与 j 都有一个恒定不变的关系
下标[ 0 , k ]的字符串与下标[j - 1 - k , j - 1 ]的字符串相同
k == -1 ,那么就说明不存在两个这样的字符串,此时,就需要i++,去匹配后面的字符

所以我们可以牺牲空间复杂度,来换取时间效率。
即创建一个next数组,里面存储子串在 j 下标匹配失败后,应该移动到什么位置,
当子串匹配失败后,主串下标 i 不变,子串下标挪动到合适的位置。


三、创建next数组

1.理论

  • 如果在子串的 0 下标处就匹配失败了,那么下一次匹配应该还是在 0 下标处匹配,那这样就会变成死循环,
  • 如果在子串的 0 下标处就匹配失败,那么i就应该往下走一个,才有可能找到匹配的字符串。所以令next[0] = -1,通过让程序识别-1来操控主串下标 i
  • 如果在子串的 1 下标处匹配失败,1 下标前就只有一个字符,不可能有两个相同字符串的。所以next[1] = 0;

那么后面的next数组就要咱们自己写算法了。


其实这个算法也不难写,在写算法之前,

我们先将《字串下标移动》中得出的一个重点重点重点的结论与next数组进行关联🤗🤗

🤪🤪🤪超级重点重点重点!!!!

假设 next[ j ] = x
x != -1 时,x 与 j 都有一个恒定不变的关系
下标[ 0 , x - 1 ]的字符串与下标[j - x , j - 1 ]的字符串相同
x == -1 ,那么就说明不存在两个这样的字符串,此时,就需要i++,去匹配后面的字符

如果有小伙伴对这个结论有些疑惑,先带着疑惑继续往下看,下面会为大家揭秘疑惑的!!


int k = 0;//代表必须以0下标开头的字符串的结尾
int j = 2;//代表必须以j-1下标结尾的字符串的结尾

在这里插入图片描述

此时第一组《子串下标移动》的B情况,
next[2] = 1;即
next[j] = k+1;
k++;
j++;

即此时的 0, 1 下标处的字符一定相同,


第二组,符合上述,字符下标的移动中的第一种情况。
next[3] = 2;

next[j] = k+1;
k++;
j++;

即此时的 0, 1, 2 下标处的字符一定相同,


此时大家应该很好奇,为什么 jk 都是字符串的结尾呢??
难道开头和中间的字符不需要比较吗??

如果有这个疑惑的小伙伴,请移步《子串下标的移动》中得出的重点重点重点结论,同时这个结论也可以解释为什么 next[ j ] = k + 1;

至于为什么要+1,是因为下标[ 0 , k ]的字符串与下标[j - 1 - k , j - 1 ]的字符串相同
所以要比较的是下标 k + 1 与 下标 j处字符。


那么当大家明白了为什么 next[ j ] = k + 1;
也就解答了超级重点重点重点的结论中的,

当next[ j ] = x时,
为什么是下标[ 0 , x - 1 ]的字符串与下标[j - x , j - 1 ]的字符串相同。


到了这里我们就需要明白,无论是《子串下标移动》中 A,B,C中的哪一种情况,

k 下标与 j - 1下标相等时,才能确定 next[ j ] 的值,等于 k + 1;

k 下标与 j - 1下标不相等时,就利用超级重点重点重点结论
去让k = next[ k ];
从而去寻找A,B,C三种情况中的一种,直到当 k == -1 时,说明A,B,C三种情况都不存在,那么他就只能从0下标重新开始匹配,并且主串下标也要+1。


🤩🤩切记next数组的意义

在一般情况下,利用k = next[ k ],进行跳跃,可以帮我们直接跳过中间不可能形成符合条件的字符串的字符,从而更加节省时间。

2.图解

在这里插入图片描述


在这里插入图片描述


在这里插入图片描述


在这里插入图片描述


在这里插入图片描述


在这里插入图片描述


在这里插入图片描述


在这里插入图片描述


在这里插入图片描述


在这里插入图片描述


在这里插入图片描述


在这里插入图片描述


在这里插入图片描述

就模拟到这里,当 k 下标与 j - 1 下标处字符不相等,让k = next[ k ] 可以让我们跳过很多不符合条件的字符,从而节省更多时间。

四、KMP完整代码

    public static int KMP(String str,String sub,int pos) {
        //判断两个串都不为空,与pos的合法性
        if(str == null || sub == null || pos > str.length() - sub.length()) {
            return -1;
        }

        //next数组,代表在子串的第j个下标匹配失败时,j应该回退到的位置。
        int[] next = new int[sub.length()];
        getNext(sub,next);


        int i = pos;//主串下标,从pos位置开始
        int j = 0;//子串下标


        while (i < str.length() && j < sub.length()) {
            //j == -1,代表字串的第一个字符就与i位置的字符不同,所以i++,去i+1的位置继续去匹配字串
            //j++ 使从子串0下标重新开始匹配。
            //如果相同,那么j与i都往后走,继续匹配后面的字符
            if(j == -1 || str.charAt(i) == sub.charAt(j)) {
                j++;
                i++;
            } else {
                //证明不是第一个字符不相同,所以按next数组中的值去回退位置
                j = next[j];
            }
        }

        //返回子串开始出现在主串的第一个下标
        if(j == sub.length()) {
            return i - j;
        }

        //走到这一步说明,子串在主串中不存在,返回-1
        return -1;
    }
    private static void getNext(String ary, int[] next) {
        next[0] = -1;

        //判断数组长度
        if(next.length <= 1) {
            return;
        }
        next[1] = 0;
        int j = 2;
        int k = 0;
        while (j < ary.length()) {
            if(k == -1 || ary.charAt(j - 1) == ary.charAt(k)) {
                next[j] = k + 1;
                j++;
                k++;
            } else {
                k = next[k];
            }
        }
        
    }

总结

以上就是今天要分享的KMP算法了,本文仅是个人对KMP算法的想法,文章中的重点重点重点理论超级重点重点重点理论一定要理解,🤗🤗🤗才能更好的明白如何创建next数组。如有错误,还请各位在评论区讨论指正。

路漫漫,不止修身也养性。

  • 6
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值