一篇博客学会KMP算法

一、BF算法

学习KMP算法之前,我们需要先学会BF算法:

BF算法,即暴力(Brute Force)算法,是普通的模式匹配算法,BF算法的思想就是将目标串S的第一个字符与模式串T的第一个字符进行匹配,若相等,则继续比较S的第二个字符和 T的第二个字符;若不相等,则比较S的第二个字符和T的第一个字符,依次比较下去,直到得出最后的匹配结果。

——百度百科

文字有点晦涩,举个例子吧:

假设字符串“ababcabcdabcde”为主串,字符串“abcd”为子串,现在需要查找子串是否在主串中出现,找到就返回主串中第一次匹配的第一个字符下标,找不到则返回-1。

 和 分别表示主串和子串的字符下标,当 和 位置的字符相同时,i ++j ++

即:

 ↓

i = 2 ,  j = 2 时,i 位置对应的字符不同,那么 就需要回到0下标的位置,而 需要回到起始位置+1的地方( 从0下标开始的,起始位置此时是0)。

 i 位置对应的字符还是不同,j 继续回退到0下标的位置,回到起始位置+1的地方(起始位置此时是1):

此时, 和 位置的字符相同i ++j ++ 

↓ 

 ↓ 

对应位置的字符不同,j 回退到0下标,用代码表示就是 j = 0,而 回退到上次起始位置+1的地方,用代码表示就是:i - j + 1,因为 j 走的步数一样多,所以 i - j 就是回到刚才的起始位置,+1就是到起始位置的下一个下标。 

当主串中找到与子串匹配的字符串时:

↓ 

此时 j 已经越界,循环结束,返回起始位置:  i -  j

如果 i 越界,说明主串中没有字符串可以和子串相匹配,循环结束,返回 -1

转换为代码:

public static int BF(String str, String sub){  //str表示主串,sub表示子串
        if(str == null || sub ==null){
            return -1;//判断字符串是否为空
        }
        int lenStr = str.length();//主串长度
        int lenSub = sub.length();//子串长度
        if(lenStr == 0 || lenSub == 0){
            return -1;//判断字符串长度是否为 0
        }

        int i = 0;//表示主串下标
        int j = 0;//表示子串下标
        while (i < lenStr && j < lenSub){
            if(str.charAt(i) == sub.charAt(j)){  //比较
                i++;
                j++;
            }else {  //回退
                i = i - j + 1;
                j = 0;
            }
        }
        if(j >= lenSub){
            return i - j;//找到了
        }
        return -1;//没找到
    }
时间复杂度分析:最坏为O(m*n); m是主串长度,n是子串长度。

二、KMP算法

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

——百度百科

与BF算法的时间复杂度相比,KMP算法显然更加高效,因为KMP算法和BF算法的最大区别就在于:KMP算法中,如果 j 的字符不同,主串的下标 i 不会回退,子串的下标 也不一定会回退到0下标位置

为什么主串的下标 i 不回退? 举个例子:

此时 i 下标的字符 和 j 下标的不同,回退到1下标位置是没有必要的, 因为它和 j 在0位置的下标还是不一样的。

如果 i 回退到1下标的位置,那么下一次它还是会到2下标的位置,所以不如不回退。

j 不一定回退到0下标的位置,那么 j 的应该回退到什么位置呢?

KMP算法的精髓就是next[ ]数组:next [ j ] = k,来表示不同的j对应的k值,这个k就是回退的下标位置。

举个栗子:

假设子串的2下标的字符和主串的不同,那么 j 可能回退到 1 下标, 如果在5下标匹配失败,那么可能会回退到 3 下标,如果在6下标匹配失败,可能会回退到 0 下标......

也就是说,子串中的每个字符的下标都有一个对应的 k 值,用来记录该下标字符如果匹配失败,它应该回退的位置;所以next[ ]数组长度和子串长度相同。

求 值规则:

1、找到匹配成功部分的两个相等的真子串(不包含本身),一个以0下标字符开始,另一个以 j - 1下标字符结尾的串。 

2、next[ ]数组的0下标默认是-1 , 1下标默认是0 。

求 k 值的规则看不懂没关系,举个栗子就懂了:

next[ ]数组0下标和1下标的 k 值默认是-1 和 0,现在我们要计算next[ ]数组中的 2 下标,也就是 下标的 k 值, 那么就需要在子串中查找是否有两个相同的以0下标字符开始,j-1下标字符结束的字符串,即以 a 开始,以 b 结束的字符串,显然是没有的,那么next[ ]数组对应的2下标的 k 值就是0。

当 = 3时,就需要找有没有两个相同的以0下标字符(a)开始,2下标字符(c)结束的字符串,也是没有,那么对应的next[ ]数组3下标的 k 值就是0。

当 = 4时,找有没有两个相同的以0下标字符(a)开始,3下标字符(a)结束的字符串,这次我们就找到了"a",这个字符串的长度是1,那么对应的next[ ]数组4下标的 k 值就是1。

当 = 5时,找有没有两个相同的以0下标字符(a)开始,4下标字符(b)结束的字符串,有!"ab",这个字符串的长度是2,那么对应的next[ ]数组4下标的 k 值就是2。

现在,我们已经求得这个子串所对应的next[ ]数组,当它和主串进行比较时,可以发现,当i = 5,

j = 5时,对应的字符不同,此时 i 不变,返回它对应的next[ j ]数组的值——2下标位置。

 ↓

  ↓

这就是整个KMP算法的逻辑,最主要的部分就是求出子串对应的next[ ]数组,这里给大家出两个求next[ ]数组的练习:

练习1:求字符串"a b a b c a b c d a b c d e"对应的next[ ]数组。
练习2:求字符串"
a b c a b c a b c a b c d a b c d e"对应的next[ ]数组。

参考答案:

练习1:-1 0 0 1 2 0 1 2 0 0 1 2 0 0 

练习2:-1 0 0 0 1 2 3 4 5 6 7 8 9 0 1 2 3 0

注意:

(1)

这样也是可以的,两个字符串可以有重复的部分。

(2)

这种计算方式是错误的。

第一个字符串必须是从0下标开始;

第二个字符串必须在 j - 1下标结束。

现在我们会手动计算next[]数组了,但是怎么把它转换成代码呢?

假设next[ j ]  =  k , 把字符串先看成一个字符数组 p

如果p [ j ]  != p [ k ]呢? 

 

以上可以推导出:如果next[ j ]  =  k,那么next[ j + 1]  =  k + 1;否则k = next [ k ] 。 

接下来我们就可以用代码写出整个KMP算法了:

public static int KMP(String str, String sub, int pos){
        if(str == null || sub == null){
            return -1;
        }
        int lenStr = str.length();
        int lenSub = sub.length();
        if(lenStr == 0 || lenSub == 0){
            return -1;
        }
        if(pos < 0 || pos >= lenStr){
            return -1;
        }
        int i = pos;//pos表示从指定下标开始向后查找
        int j = 0;
        int[] next = new int[lenSub];
        setNext(next,sub,lenSub);//初始化next数组
        while (i<lenStr && j < lenSub){
            if(j == -1 || str.charAt(i)==sub.charAt(j)){
                i++;
                j++;
            }else {
                j = next[j];
            }
        }
        if(j >= lenSub){
            return i - j;
        }
        return -1;
    }
    public static void setNext(int[] next, String sub, int lenSub){
        next[0] = -1;
        next[1] = 0;
        int j = 2;//从2下标开始设置,
        int k = next[1];//k是j的前一项的k值
        while (j < lenSub){
            if(k == -1 || sub.charAt(j) == sub.charAt(k)){
                next[j] = k + 1;
                j++;
            }else {
                k = next[k];
            }
        }
    }

next[ ]数组的优化:

假设有这样一个字符串"a a a a a a a a a b",

它的next[ ]数组是-1,0,1,2,3,4,5,6,7,8

假设在5下标匹配失败了,那么它回退位置的字符还是a,还是匹配失败,还需要一步一步地回退,效率很低,所以我们需要对next[ ]数组进行优化,计算优化之后的nextVal[ ]数组。

nextVal[ ]数组求法很简单,如果当前下标 i 回退位置的字符,正好和当前下标 i 对应的字符相同,

那么nextVal[ i ] = next[ next[ i ] ]

如果不同,则nextVal[ i ] = next[ i ]

转换为代码: 

public static void setNextVal(int[] nextVal,int[] next, String sub){
        nextVal[0] = -1;
        nextVal[1] = 0;
        for (int i = 2; i < sub.length(); i++) {
            if(sub.charAt(i) == sub.charAt(next[i])){
                nextVal[i] = next[next[i]];
            }else {
                nextVal[i] = next[i];
            }
        }
    }

优化后的KMP算法的代码:

    public static int KMP(String str, String sub, int pos){
        if(str == null || sub == null){
            return -1;
        }
        int lenStr = str.length();
        int lenSub = sub.length();
        if(lenStr == 0 || lenSub == 0){
            return -1;
        }
        if(pos < 0 || pos >= lenStr){
            return -1;
        }
        int i = pos;//pos表示从指定下标开始向后查找
        int j = 0;
        int[] next = new int[lenSub];
        setNext(next,sub,lenSub);//初始化next数组

        int[] nextVal = new int[lenSub];
        setNextVal(nextVal,next,sub);

        while (i<lenStr && j < lenSub){
            if(j == -1 || str.charAt(i)==sub.charAt(j)){
                i++;
                j++;
            }else {
                j = nextVal[j];//j回退到nextVal数组中j下标的位置
            }
        }
        if(j >= lenSub){
            return i - j;
        }
        return -1;
    }
    public static void setNext(int[] next, String sub, int lenSub){
        next[0] = -1;
        next[1] = 0;
        int j = 2;//从2下标开始设置,
        int k = next[1];//k是j的前一项的k值
        while (j < lenSub){
            if(k == -1 || sub.charAt(j) == sub.charAt(k)){
                next[j] = k + 1;
                j++;
            }else {
                k = next[k];
            }
        }
    }
    public static void setNextVal(int[] nextVal,int[] next, String sub){
        nextVal[0] = -1;
        nextVal[1] = 0;
        for (int i = 2; i < sub.length(); i++) {
            if(sub.charAt(i) == sub.charAt(next[i])){
                nextVal[i] = next[next[i]];
            }else {
                nextVal[i] = next[i];
            }
        }
    }

不对的地方欢迎大佬指正~

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

吃点橘子

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

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

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

打赏作者

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

抵扣说明:

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

余额充值