KMP算法(一):正常逻辑求解(next数组)

一、KMP是什么

KMP算法是为了解决字符串匹配效率而提出的,提出者为D.E.Knuth、J,H,Morris 和 V.R.Pratt 三位大牛,故称为“KMP”算法。

二、暴力求解算法

1、题目:假设一个父字符串是father,子字符串是son,在father中查找son,如果存在则返回son在father中的起始索引,不存在则返回-1。

2、最简单的解法就是使用循环,挨个字符比较,如果匹配就继续,如果不匹配则father和son同时回退前进的长度,重新计算。java代码如下:

/**
 * 暴力算法
 */
private static int violentMatch(String father, String son) {
    int fatherLength = father.length();
    int sonLength = son.length();
    //i记录在father中的位置
    int i = 0;
    //i记录在son中的位置
    int j = 0;
    while (i < fatherLength && j < sonLength) {
        if (father.charAt(i) == son.charAt(j)) {
            //如果父子当前的字符匹配成功,则同时前进以为,匹配下一个
            i++;
            j++;
        } else {
            //匹配不成功,则回退前进的长度
            //因为j每次都是从son的第一位开始,也就是0,所以i-j就是回到这一轮开始的位置,再+1则表示下一个字符(当前轮开始的字符匹配不成功)
            i = i - j + 1;
            j = 0;
        }
    }
    //如果跳出循环之后,j等于子串的长度(循环进行的条件之一是j < sonLength,匹配成功后j++ 就等于sonLength了)
    if (j == sonLength) {
        //此时i所在位置是son在father中的最后一个字符的后一位,所以起始位置要减去j,就是son在father中的第一位
        return i - j;
    }
    //没有匹配成功就返回-1
    return -1;
}

3、暴力求解的问题在哪呢?

在于i跟j同步回溯,会造成很多不必要的对比次数。比如father=“abcdef", son="abx",

 

第一次开始比较的时候,到father的 c 位置就发现不匹配,结束回退再次循环,但是其实人工来看完全不需要从 b 和 c 再开始了,因为第一次比较已经知道了father 的 b 和 c 与son的 a 是不一样的,更何况son的 x 在father的前三位压根就没有,完全可以从 d 开始。

三、KMP算法之正常逻辑求解(next数组)

(一)、基本算法流程

假设father匹配到 i 位置,son匹配到 j 位置。

-如果j = -1, 或者当前匹配字符成功(father.charAt(i) == son.charAt(j)), 都令i++, j++, 继续匹配下一个字符。

-如果j != -1, 且当前字符匹配失败,则令 i 不变,j=next[j], 意思是当前字符匹配失败,son相对于father向右移动了j - next[j] 位。就比如father="abcabcd", son="abcd", next[]=[-1, 0, 0, 0 ],匹配到father.charAt(3)时,father的字符是a,son的字符是d,i = 3, j = 3,这个时候如果暴力解法,则i = 1, j = 0,而KMP算法则是将 i 不变, j = next[j] = 0,意味着将son右移 j - next[j] = 3。

用代码表示如下:

/**
 * KMP算法
 */
private static int match(String father, String son) {
    //通过子串计算next数组
    int[] next = getNext(son);
    int i = 0, j = 0;
    //一直循环到i等于father长度,或者j等于son长度
    while (i <= father.length() - 1 && j <= son.length() - 1) {
        //-1表示son需要从头匹配,||后面的语句表示字符相等,后移一位
        if (j == -1 || father.charAt(i) == son.charAt(j)) {
            i++;
            j++;
        } else {
            //字符不匹配,i不变,j回退到next[j]进行比较,或者说son右移j - next[j]位
            j = next[j];
        }
        if (j > son.length() - 1) {
            //匹配成功
            return i - son.length();
        }
    }
    return -1;
}

(二)、next数组计算原理

1、寻找前缀后缀最长公共元素长度(这里看到一篇博客说的比较详细,直接引用了,链接:https://blog.csdn.net/v_july_v/article/details/7041827

2、然后自己再解释一下,为什么“next 数组考虑的是除当前字符外的最长相同前缀后缀”,见下图

(三)、next计算代码

private static int[] getNext(String son) {
    //因为是自己跟自己比较,所以起始状态j 比 i小1。只是-1不存在,所以下方next[0]设置成了-1
    //配合判断条件j == -1,造成的结果就是i和j一起进一位。如果之前的步骤两个字符相同,那就是都进一位继续比较。
    // 如果不同,j置为-1然后自增变成0,i进一位,就开始了多一位字符的运算。
    int i = 0, j = -1;
    int[] next = new int[son.length()];
    next[0] = -1;
    while (i < son.length() - 1) {
        //以son = "ababd"为例
        //起始的时候j=-1,令i = 1, j = 0,所以next[1] = 0,而且不管是什么字符串,都会是0,
        // 因为next 数组考虑的是除当前字符外的最长相同前缀后缀。所以i = 1的时候,next数组考虑的只有son[0]这一个字符
        //而当i=1了,j=0,son[1]和son[0]不相等,则j就要回退,回退到next[j],此处原理与match时候的j = next[j]的原理很相似。
        if (j == -1 || son.charAt(i) == son.charAt(j)) {
            i++;
            j++;
            next[i] = j;
        } else {
            //son[i]和son[j]不相等,则j就要回退,回退到next[j],此处原理与match时候的j = next[j]的原理很相似。
            // 回退到j = 0时就相当于此时的字符与son[0]开始比较,不相等则j=next[0]=-1,紧接着就各自加1,开始多一位字符长度的比较
            j = next[j];
        }
    }
    //感兴趣的可以再用son="abcdabcdcab"等进行debug观察计算和回溯过程
    return next;
}

 

四、自述

这周开始在看《大话数据结构》,这算是书中目前为止遇到的第一个真正意义上的算法吧,就把我难住了,大概看了一天半吧才有了大概的轮廓,但是感觉细节还不是很清晰,就想着通过写博客的方式来讲述,加深理解。如果大家觉得哪里讲的有问题或者不够透测,可以留言讨论。

边写博客边加深理解,也算是耗费了一些时间,一周就这样过去了(阶段性空闲,程序员都懂的,可别说我工作不饱和好吧……^_^)。本来想再把知乎上看到的另一种解法--确定有限状态机 也写在这里的,但是时间来不及了,毕竟还是要工作的。就准备等周末来写好了。然后下周再研究KMP算法的改进算法--nextval数组方式,以及Sunday算法。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值