Java数据结构彻底理解关于KMP算法

大家好,前面的有一篇文章讲了子序列和全排列问题,今天我们再来看一个比较有难度的问题。那就是大名鼎鼎的KMP算法。

本期文章源码:GitHub源码

简介

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

说的简单的一点,就是一个用于在主串中,查找子串的算法。

暴力解法(BF)

在讲解KMP算法之前,我们得先理解暴力解法,因为KMP算法就是在暴力解法的基础之上,进行了优化,使之匹配速度加快。

人如其名,暴力解法,就是一种很暴力的解决方法。比如:主串“abbabbec”,待查找的子串为“abbec”, 请问主串中是否包含这个子串?

我们肉眼,能够很轻松的看到从第4个字符开始,一直到末尾这一段,就是需要查找的子串。那么编译器是如何进行查找呢?它就只能一个字符一个字符的尝试:

上图就是暴力解法的全部过程,通过匹配一个个字符,如果当前字符匹配成功,i和j就同时走一步;如果当前字符匹配不成功,i 就应该回到当前开始的第一个字符的下一个字符:比如图中步骤4和步骤5,就是说的这个意思,当前是从第一个字符a开始匹配的,此时匹配失败了,i就应该回到a字符的下一个字符,j就从0下标开始,继续往下匹配;

当i或者j走到了字符串的末尾,我们就结束循环。然后判断j是不是遍历完了待查找的子串,如果确实是走到了子串的末尾,说明查找成功了,就返回子串在主串的起始位置(i - j, 在上图就是返回3),反之就是:主串的字符都遍历完了,子串的还没遍历完,那就是主串没有这个子串的情况,此时返回-1即可。

//BF算法
//s 为主串
//m 为待查找的子串
public int BF(String s, String m) {
    if(s == null || m == null) {
        return -1;
    }

    char[] str1 = s.toCharArray(); //主串
    char[] str2 = m.toCharArray(); //子串
    int i = 0; //指向主串的下标
    int j = 0; //指向子串的下标

    //i和j的值,都还没到末尾,循环继续
    while (i < str1.length && j < str2.length) {
        if (str1[i] == str2[j]) { //当前字符匹配成功
            i++;
            j++;
        } else { //当前字符匹配不成功
            i = i - j + 1; //回到开头的下一个字符
            j = 0; //子串从头开始
        }
    }

    //判断是否遍历完了子串,并返回相应的值
    return j == str2.length? i - j : -1;
}

时间复杂度:

假设主串的长度为N, 子串的长度为M。最差的情况,就如上图的情况,只有在最后一个字符才查找成功。所以子串要从每一个字符作为起始位置开始匹配,每一个字符作为起始,都会匹配子串的长度M次,也就是说暴力解法的时间复杂度为O(N*M)。

KMP算法

KMP算法和BF算法,在代码上差别不大。在BF的代码基础之上,需要计算一个next数组,在while循环里面再加一个判断语句即可,其他的代码都是不变的。

虽然代码改动不是很大,但是从逻辑思维上,却是一个质的飞越。所以我们先来看一下KMP的核心:next数组。

next数组究竟是什么?我相信很多同学都会有这样一个疑问。

其实next数组,计算的待查找的子串的每个字符(不包括当前字符)前面的所有字符中,前缀字符串和后缀字符串相等的,并且最长的字符个数。比如:

举一个完整的例子吧,比如子串是str2 = “ababab”,计算这个字符串的next数组如下:

首先就是从头开始遍历字符串:(建议拿出笔和纸,一起画一画,更容易理解)

  • j = 0; str2[j] = a

a字符前面没有任何字符,人为规定第一个字符的next值为-1。

  • j = 1; str2[j] = b

b字符前面只有一个字符a,有人就会说,那么next就是1咯? 当然不是,我们还忽略了一个重要条件:计算的当前字符前面的所有字符,进行划分前缀和后缀字符串,但是所谓的前缀和后缀字符串,并不包括了这前面的整体字符串。说的简单一点就是:假设b字符前面的所有字符是“abc”, 前缀字符串是“abc”, 后缀字符串是“abc”,这种情况是不算在内的。用数学公式解释:前缀字符串 = 后缀字符串 = b字符前面所有字符。这种情况不算数。

所以当前这个b字符,前面就只有一个字符,所以人为规定第二个字符的next值为0。

  • j = 2; str2[j] = a

a字符前面有字符串“ab”, 前缀和后缀字符串,排除“ab” = “ab”的情况,就没有能够匹配前缀和后缀的字符串,所以第三个字符的next值为0。

  • j = 3; str2[j] = b

b字符前面的字符串是“aba”,此时排除“aba” = ”aba“的情况, 那就只剩下前缀字符串“a” = 后缀字符串“a”的情况,所以第四个字符的next值为1。

  • j = 4; str2[j] = a

a字符前面的字符串是“abab”,排除“abab” = “abab”的情况后,就只剩下前缀字符串“ab” = 后缀字符串“ab”的情况,所以第五个字符的next值为2。

  • j = 5;str2[j] = b

b字符前面的字符串是“ababa”, 排除“ababa” = “ababa”的情况后,就只剩下前缀字符串“aba” = 后缀字符串“aba”,所以第六个字符的next值为3。

  • j = 6; str2[j] = ‘\0'

遍历结束

所以上面例子的next数组的值,如下图:

那么就有同学会纳闷了,在逻辑层面上,我知道该怎么计算next数组了,但是在代码层面上,似乎并没有什么思路。不急,我们现在就说一下代码怎么实现这个逻辑。

我们以上图的最后一个字符b为例。在求解b字符所对应的next值时,b字符前面的字符,是已经计算好了next值的。所以我们需要借助b字符前面已经计算好的next值,来计算b字符的next值。

所以next数组的计算代码,如下:

public int[] getNextArr(String m) {
    if (m.length() < 2) { //只有一个字符
        return new int[] {-1};
    }
    if (m.length() < 3) { //只有两个字符
        return new int[] {-1, 0};
    }

    char[] str = m.toCharArray();
    int[] next = new int[m.length()];
    next[0] = -1;
    next[1] = 0; //人为规定的两个位置的next值
    int cn = 0; //表示往前跳的下标。也就是前缀字符串的下一个字符。初始化就是第一个字符
    int i = 2; //从第三个字符开始判断

    while (i < m.length()) {
        if (str[i - 1] == str[cn]) {
            //当前字符的前一个字符,与cn所对应的字符比较
            next[i++] = ++cn; //等价于:next[i++] = next[i - 1] + 1; cn++;
        } else if (cn > 0) {
            //说明当前字符没匹配成功,cn要往前跳,找上一组前缀、后缀字符串
            cn = next[cn];
        } else {
            //cn一直往前跳,都没能与当前字符匹配成功,则当前位置next值就是0
            next[i++] = 0;
        }
    }

    return next; //返回next数组
}

next数组的计算代码,我们就讲完了,再加把劲,就还剩最后的一点点了。

我们先来将大致的框架写出来:

//KMP算法
// s 为主串
// m 为待查找的子串
public int KMP(String s, String m) {
    if (s == null || m == null || s.length() < 1 || m.length() < 1) {
        return -1;
    }

    int[] next = getNextArr(m); //计算子串的next数组

    char[] str1 = s.toCharArray();
    char[] str2 = m.toCharArray();
    int i = 0; //指向主串的下标
    int j = 0; //指向子串的下标

    while (i < str1.length && j < str2.length) {
        if (str1[i] == str2[j]) { //字符相等的情况,i和j同时加1
            i++;
            j++;
        } else if (j == 0) {

        } else {

        }
    }

    return j == str2.length? i - j : -1; //判断子串是否遍历完了
}

现在就还剩下while循环里面,20行~24行的代码,还没填写。

20行~24行的代码,是在上面的if语句没有为真,才会走到20行后面代码。也就是说此时当前的字符已经没有匹配成功了。此时就需要对j或者i进行调整。

那么对于j来说,就分为两个情况:

j != 0时,则说明j还能往前面跳,即就是说j位置前面的字符,还有可能会有前缀、后置字符串。那么j继续往前跳即可。j = next[j]。(找j位置前面的前缀字符串)

j == 0时,则说明j前面已经没有字符了,换个角度说,对于当前i位置的字符,在子串中j已经走到了第一个字符,都还没匹配成功,则说明,当前i位置的字符肯定不会匹配成功的。那么i往后走一个字符的位置,然后再循环就进行判断。

完整的代码:

//KMP算法
// s 为主串
// m 为待查找的子串
public int KMP(String s, String m) {
    if (s == null || m == null || s.length() < 1 || m.length() < 1) {
        return -1;
    }

    int[] next = getNextArr(m); //计算子串的next数组

    char[] str1 = s.toCharArray();
    char[] str2 = m.toCharArray();
    int i = 0; //指向主串的下标
    int j = 0; //指向子串的下标

    while (i < str1.length && j < str2.length) {
        if (str1[i] == str2[j]) { //字符相等的情况,i和j同时加1
            i++;
            j++;
        } else if (j == 0) { //j不能再往前走了,那么i就往后走一个位置
            i++;
        } else { //j还能往前跳,寻找前面的前缀字符串
            j = next[j];
        }
    }

    return j == str2.length? i - j : -1; //判断子串是否遍历完了
}

只要理解了next数组是如何进行计算的,那么KMP算法的就理解了一大半了。

后面的整体的代码框架,跟BF算法是差不多的。只需分为i和j两个值的调整,即可!

具体的,大家可以再分别举几个例子,自己手动去走一遍代码,这里就能更好的理解KMP算法的思路。也可以看一下左程云老师所写的《程序员代码面试指南》一书,也有相应的讲解。

好啦,本期更新就到此结束啦,我们下期见!!!

到此这篇关于Java数据结构彻底理解关于KMP算法的文章就介绍到这了,更多相关Java KMP内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • 如何通过Java代码实现KMP算法

    这篇文章主要介绍了如何通过Java代码实现KMP算法,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 KMP算法是一种改进的字符串匹配算法,由D.E.Knuth,J.H.Morris和V.R.Pratt同时发现,因此人们称它为克努特--莫里斯--普拉特操作(简称KMP算法).KMP算法的关键是利用匹配失败后的信息,尽量减少模式串与主串的匹配次数以达到快速匹配的目的.具体实现就是实现一个next()函数, 函数本身包含了模式串的局部匹配信息.时间复

  • java 实现KMP算法

    KMP算法是一种神奇的字符串匹配算法,在对 超长字符串 进行模板匹配的时候比暴力匹配法的效率会高不少.接下来我们从思路入手理解KMP算法. 在对字符串进行匹配的时候我们最容易想到的就是一个个匹配,类似下面这种: 换成Java代码就是: public static boolean bfSearch(String pattern,String txt){ if (txt.length() < pattern.length()) return false; for (int i = 0; i < t

  • 关于 Java 的数据结构链表

    目录 数据结构关于 Java 的链表 1. 删除链表中等于给定值 val 的所有节点 2. 反转一个单链表 3. 给定一个带有头结点 head 的非空单链表 4. 输入一个链表,输出该链表中倒数第k个结点 5. 有序链表合并为一 6. 编写代码 7. 删除该链表中重复的结点 8. 链表的回文结构 9. 输入两个链表,找出它们的第一个公共结点 10. 给定一个链表,判断链表中是否有环 11. 给定一个链表 数据结构关于 Java 的链表 1. 删除链表中等于给定值 val 的所有节点 class

  • java 中模式匹配算法-KMP算法实例详解

    java 中模式匹配算法-KMP算法实例详解 朴素模式匹配算法的最大问题就是太低效了.于是三位前辈发表了一种KMP算法,其中三个字母分别是这三个人名的首字母大写. 简单的说,KMP算法的对于主串的当前位置不回溯.也就是说,如果主串某次比较时,当前下标为i,i之前的字符和子串对应的字符匹配,那么不要再像朴素算法那样将主串的下标回溯,比如主串为"abcababcabcabcabcabc",子串为"abcabx".第一次匹配的时候,主串1,2,3,4,5字符都和子串相应的

  • Java数据结构彻底理解关于KMP算法

    大家好,前面的有一篇文章讲了子序列和全排列问题,今天我们再来看一个比较有难度的问题.那就是大名鼎鼎的KMP算法. 本期文章源码:GitHub源码 简介 KMP算法是一种改进的字符串匹配算法,由D.E.Knuth,J.H.Morris和V.R.Pratt提出的,因此人们称它为克努特-莫里斯-普拉特操作(简称KMP算法).KMP算法的核心是利用匹配失败后的信息,尽量减少模式串与主串的匹配次数以达到快速匹配的目的.具体实现就是通过一个next()函数实现,函数本身包含了模式串的局部匹配信息.KMP算法

  • Java 数据结构深入理解ArrayList与顺序表

    目录 一.ArrayList简介 二.ArrayList的使用 1.ArrayList的构造 2.ArrayList的遍历 3.ArrayList的常见操作(方法) 4.ArrayList的扩容机制 三.模拟实现一个顺序表(Object[]) 一.ArrayList简介 在集合框架中,ArrayList是一个普通的类,实现了List接口,具体框架图如下: ArrayList底层是一段连续的空间,并且可以动态扩容,是一个动态类型的顺序表. 二.ArrayList的使用 1.ArrayList的构造

  • Java数据结构实现折半查找的算法过程解析

    折半查找技术,也就是二分查找,通常称为二分法查找.它的前期是线性表中的记录必须是关键码有序(通常从大到小有序),线性表必须采用顺序存储.折半查找的基本思想是: 取中间记录作为比较对象,若给定值与中间记录的关键字,则在中间记录的关键字相等,则查找成功:若给定值小于中间记录的做半,去继续查找:若给定值大于中间记录的关键字,则在中间记录的右半区继续查找.不断重复上述过程,直到查找成功,或所有查找区域无记录,查找失败为止. 在文本排重中需要用到折半查找,需要查找一个数组中是否存在某个数. 算法维护着一个

  • Java数据结构与算法入门实例详解

    第一部分:Java数据结构 要理解Java数据结构,必须能清楚何为数据结构? 数据结构: Data_Structure,它是储存数据的一种结构体,在此结构中储存一些数据,而这些数据之间有一定的关系. 而各数据元素之间的相互关系,又包括三个组成成分,数据的逻辑结构,数据的存储结构和数据运算结构. 而一个数据结构的设计过程分成抽象层.数据结构层和实现层. 数据结构在Java的语言体系中按逻辑结构可以分为两大类:线性数据结构和非线性数据结构. 一.Java数据结构之:线性数据结构 线性数据结构:常见的

  • KMP算法最浅显理解(小白教程)

    说明 KMP算法看懂了觉得特别简单,思路很简单,看不懂之前,查各种资料,看的稀里糊涂,即使网上最简单的解释,依然看的稀里糊涂. 我花了半天时间,争取用最短的篇幅大致搞明白这玩意到底是啥. 这里不扯概念,只讲算法过程和代码理解: KMP算法求解什么类型问题 字符串匹配.给你两个字符串,寻找其中一个字符串是否包含另一个字符串,如果包含,返回包含的起始位置. 如下面两个字符串: char *str = "bacbababadababacambabacaddababacasdsd"; char

  • Java数据结构之KMP算法详解以及代码实现

    目录 暴力匹配算法(Brute-Force,BF) 概念和原理 next数组 KMP匹配 KMP全匹配 总结 我们此前学了前缀树Trie的实现原理以及Java代码的实现.Trie树很好,但是它只能基于前缀匹配实现功能.但是如果我们的需求是:一个已知字符串中查找子串,并且子串并不一定符合前缀匹配,那么此时Trie树就无能为力了. 实际上这种字符串匹配的需求,在开发中非常常见,例如判断一个字符串是否包括某些子串,然后进行分别的处理. 暴力匹配算法(Brute-Force,BF) 这是最常见的算法字符

  • Java数据结构与算法之双向链表、环形链表及约瑟夫问题深入理解

    目录 一.双向链表 二.环形链表及其应用:约瑟夫问题 环形链表图示 构建一个单向的环形链表思路 遍历环形链表 约瑟夫问题 一.双向链表 使用带head头的双向链表实现 - 水浒英雄排行榜管理单向链表的缺点分析: 单向链表,查找的方向只能是一个方向,而双向链表可以向前或者向后查找. 单向链表不能自我删除,需要靠辅助节点,而双向链表,则可以自我删除,所以前面我们单链表删除节点时,总是找到temp,temp时待删除节点的前一个节点(认真体会). 分析双向链表的遍历,添加,修改,删除的操作思路 1.遍历

  • Java数据结构与算法之单链表深入理解

    目录 一.单链表(Linked List)简介 二.单链表的各种操作 1.单链表的创建和遍历 2.单链表的按顺序插入节点 以及节点的修改 3.单链表节点的删除 4.以上单链表操作的代码实现 (通过一个实例应用) 三.单链表常见面试题 1.求单链表中节点的个数 2.查找单链表中的倒数第K个节点[新浪面试题] 3.单链表的反转[腾讯面试题,有点难度] 4.从尾到头打印单链表 一.单链表(Linked List)简介 二.单链表的各种操作 1.单链表的创建和遍历 2.单链表的按顺序插入节点 以及节点的

  • Java数据结构与算法之稀疏数组与队列深入理解

    目录 一.数据结构和算法简介 二.稀疏数组 稀疏数组的应用实例 二维数组与稀疏数组的转换 二维数组 转 稀疏数组的思路 稀疏数组 转 原始的二维数组的思路 三.队列 数组模拟队列 代码优化:数组模拟环形队列 之前学完了Java SE的知识,掌握了面向对象的编程思想,但对集合.多线程.反射.流的使用等内容理解的还不是很深入,打算再学习数据结构与算法的同时,在空闲的时间里去图书馆看<Java核心技术 卷 I>这本书,很多大佬对这本书很推崇,之前在图书馆也看过其他Java的书籍,经过对比,这本书确实

随机推荐