Java数据结构之KMP算法的实现

目录
  • 问题介绍
  • 暴力求解
  • 知识补充
  • Next示例
  • Next代码
  • 匹配示例
  • 匹配代码
  • 完整代码

本次我们介绍数据结构中的KMP算法,我们会从下面几个角度来介绍:

问题介绍

首先我们先介绍适用于KMP算法的问题:

  • 给定一个字符串S,以及一个模式串P,所有字符串中只包含大小写英文字母以及阿拉伯数字。
  • 模式串P在字符串S中多次作为子串出现。
  • 求出模式串P在字符串S中所有出现的位置的起始下标。

我们给出一个问题的简单示例:

// 输入 p长度 p s长度 s
3
aba
5
ababa

// 输出结果
0 2

暴力求解

所有问题我们都是在暴力求解的基础上进行更新迭代的,所以我们首先给出暴力求解:

// 下面为伪代码,只是起到思路作用

// 首先我们需要创造s[],p[],并赋值
S[N],P[N]

// 然后我们开始匹配,我们会从S的第一个字符开始匹配,设置一个flag判断该字符开始的字符串是否与P字符匹配
// 该算法从每个i开始,全部进行匹配
for(int i = 1;i <= n;i++ ){
    boolean flag = true;
    for(int j = 1;j <= m;j++){
        if(s[i+j-1] != p[j]){
            flag = false;
            break;
        }
    }
}

// 我们给出一套完整的暴力求解方法

/**

 * 暴力破解法

 * @param ts 主串

 * @param ps 模式串

 * @return 如果找到,返回在主串中第一个字符出现的下标,否则为-1

 */

public static int bf(String ts, String ps) {

    char[] t = ts.toCharArray();

    char[] p = ps.toCharArray();

    int i = 0; // 主串的位置

    int j = 0; // 模式串的位置

    while (i < t.length && j < p.length) {

       if (t[i] == p[j]) { 

           // 当两个字符相同,就比较下一个
           i++;
           j++;

       } else {

           i = i - j + 1; // 一旦不匹配,i后退(从之前i的下一位开始,也是遍历所有i)

           j = 0; // j归0
       }
    }

    // 当上面循环结束,必定是i到头或者j到头,如果是j到头,则说明存在子串符合父串,我们就将头位置i返回
    if (j == p.length) {
       return i - j;
    } else {
       return -1;
    }

}

// 但是我们会发现:我们可以不让i回退而是让j回退,使j回退到能够与当前i相匹配的点位,然后继续进行主串和模式串的匹配

首先我们会发现这个算法的时间复杂度为O(n^2)

我们其中可以优化的点就是i的位置更新,我们可以根据p字符串的特性来判断i在失败后最近可以移动到哪个点位!

知识补充

我们为了学习KMP算法,我们需要补充一些下面会用到的知识:

  • s[ ]是模式串,即比较长的字符串。
  • p[ ]是模板串,即比较短的字符串。(这样可能不严谨。。。)
  • “非平凡前缀”:指除了最后一个字符以外,一个字符串的全部头部组合。
  • “非平凡后缀”:指除了第一个字符以外,一个字符串的全部尾部组合。(后面会有例子,均简称为前/后缀)
  • “部分匹配值”:前缀和后缀的最长共有元素的长度。
  • next[ ]是“部分匹配值表”,即next数组,它存储的是每一个下标对应的“部分匹配值”,是KMP算法的核心。(后面作详细讲解)。

我们所用到的思想是:

  • 在每次失配时,不是把p串往后移一位,而是把p串往后移动至下一次可以和前面部分匹配的位置,这样就可以跳过大多数的失配步骤
  • 而每次p串移动的步数就是通过查找next[ ]数组确定的

Next示例

我们给出一个简单的Next示例:

// 首先我们给出一个next手写实例

/*
模板串为:ABABAA

next[0]代表t[0]-t[0],即"A" , "A"的前缀和后缀都为空集,共有元素的长度为0.

next[1]代表t[0]-t[1],即"AB",前缀为“A”,后缀为“B”,共有元素的长度为0..

next[2]代表t[0]~t[2],即"ABA",前缀为“AB",后缀为"BA",最大前后缀即"A",长度为1.

next[3]代表t[0]~t[3],即"ABAB",前缀为"ABA"后缀为"BAB”,最大前后缀即"AB ",长度为2.

next[4]代表t[0]~t[4],即"ABABA",前缀为"ABAB",后缀为"BABA",最大前后缀即" ABA",长度为3.

next[5]代表t[0]-t[5],即" ABABAA",前缀为“ABABA",T后缀为“BABAA";最大前后缀即"A",长度为1.

*/

// 我们next的作用是使next[j]=k使 P[0 ~ k-1] == P[j-k ~ j-1]、
// 当第n个数不匹配时,我们让j回退到k,这时我们的主串和模式串的前缀还属于匹配状态,我们继续进行匹配
例如 ababc
    我们如果匹配到c不符合时,我们可以使j回退到k(这里的k是2,即a)再继续进行匹配
    因为我们的c前面的ab和开头的ab是匹配的,我们主串中的i前面肯定也是ab,我们的l前面也是ab,所以两者匹配,我们可以继续后面的匹配
    相当于我们的x不变,我们将j放在2的位置,前面的ab已完成匹配,我们只需要匹配abc即可

// 公式书写就是下述:

当T[i] != P[j]时

有T[i-j ~ i-1] == P[0 ~ j-1]

由P[0 ~ k-1] == P[j-k ~ j-1]

必然:T[i-k ~ i-1] == P[0 ~ k-1]

Next代码

我们给出求解Next的代码展示:

public static int[] getNext(String ps) {

    char[] p = ps.toCharArray();

    int[] next = new int[p.length];

    // 这里的next[0]需要等于-1
    // 因为j在最左边时,不可能再移动j了,这时候要应该是i指针后移。所以在代码中才会有next[0] = -1;这个初始化。
    next[0] = -1;

    // 这里设置j的初始值从第一个开始(我们需要得到全部next数组)
    int j = 0;

    // 这里设置k,k就是应该返回的位置,也就是我们常说的前缀和后缀匹配区域的前缀的后一个位置
    int k = -1;

    // 进行循环,得到next数组
    while (j < p.length - 1) {

        // 首先是k==-1时,说明前面已无匹配状态,我们重新开始
        // 然后是p[j] == p[k],说明循环时新添加的值,与我们应该返回比对的位置相同
        // 同时由于我们之前的部分都是已经匹配成功的,所以加上这个数使我们的匹配长度又增加一位
       if (k == -1 || p[j] == p[k]) {

           // 当两个字符相等时要跳过(因为p[k]与S[i]不符合的话,由于我们的p[j]=p[k],所以肯定也不符合,我们直接跳下一步)
           if (p[++j] == p[++k]) { 

              next[j] = next[k];

           } else {
			// 因为在P[j]之前已经有P[0 ~ k-1] == p[j-k ~ j-1]。(next[j] == k)
			// 这时候现有P[k] == P[j],我们是不是可以得到P[0 ~ k-1] + P[k] == p[j-k ~ j-1] + P[j]。
       		// 即:P[0 ~ k] == P[j-k ~ j],即next[j+1] == k + 1 == next[j] + 1
            // 前面我们已经进行了j++和k++,所以这里直接赋值即可
              next[j] = k;

           }

       } else {
		// 如果当前状态无法匹配,我们就跳回上一个前缀后缀相同部分再来判断是否前后缀相同
           k = next[k];

       }

    }

    return next;

}

匹配示例

我们给出简单的匹配示例:

匹配相对而言就比较简单了

主串:abababc

模式串:abc

我们首先进行i++,j++范围的匹配,当第三位,即a和c匹配不成功时,我们不移动i,而是移动j

我们将j=2,移动到j=0,即next[2]的位置,在之后一直匹配并再对j进行一次移动,到最后匹配成功为止

匹配代码

我们给出对应的匹配代码:

/*该代码实际上是由暴力求解代码改造过来的*/

public static int KMP(String ts, String ps) {

    char[] t = ts.toCharArray();

    char[] p = ps.toCharArray();

    int i = 0; // 主串的位置

    int j = 0; // 模式串的位置

    int[] next = getNext(ps);

    // 开始判断(设置边界值)
    while (i < t.length && j < p.length) {

        // 当j为-1时,要移动的是i,当然j也要归0
        // 如果匹配成功,两者都进行移动,开始下一位比对
       if (j == -1 || t[i] == p[j]) { 

           i++;

           j++;

       } else {
		   // 如果比对失败,我们将 j 返回next数组指定位置继续匹配

           // i不需要回溯了
           // i = i - j + 1;

           j = next[j]; // j回到指定位置

       }

    }

    // 最后同样进行判断,是否符合条件
    if (j == p.length) {

       return i - j;

    } else {

       return -1;

    }

}

完整代码

最后为大家展示一下完整代码:

import java.util.Scanner;

class ppp {

    /**
     * 主代码
     * @param args
     */
    public static void main(String[] args) {

        Scanner scanner = new Scanner(System.in);

        String ts = scanner.nextLine();

        String ps = scanner.nextLine();

        int kmp = KMP(ts, ps);

        System.out.println(kmp);
    }

    /**
     * kmp算法
     * @param ts
     * @param ps
     * @return
     */
    public static int KMP(String ts, String ps) {

        char[] t = ts.toCharArray();

        char[] p = ps.toCharArray();

        int i = 0; // 主串的位置

        int j = 0; // 模式串的位置

        int[] next = getNext(ps);

        // 开始判断(设置边界值)
        while (i < t.length && j < p.length) {

            // 当j为-1时,要移动的是i,当然j也要归0
            // 如果匹配成功,两者都进行移动,开始下一位比对
            if (j == -1 || t[i] == p[j]) {

                i++;

                j++;

            } else {
                // 如果比对失败,我们将 j 返回next数组指定位置继续匹配

                // i不需要回溯了
                // i = i - j + 1;

                j = next[j]; // j回到指定位置

            }

        }

        // 最后同样进行判断,是否符合条件
        if (j == p.length) {

            return i - j;

        } else {

            return -1;

        }

    }

    /**
     * next数组求解
     * @param ps
     * @return
     */
    public static int[] getNext(String ps) {

        char[] p = ps.toCharArray();

        int[] next = new int[p.length];

        // 这里的next[0]需要等于-1
        // 因为j在最左边时,不可能再移动j了,这时候要应该是i指针后移。所以在代码中才会有next[0] = -1;这个初始化。
        next[0] = -1;

        // 这里设置j的初始值从第一个开始(我们需要得到全部next数组)
        int j = 0;

        // 这里设置k,k就是应该返回的位置,也就是我们常说的前缀和后缀匹配区域的前缀的后一个位置
        int k = -1;

        // 进行循环,得到next数组
        while (j < p.length - 1) {

            // 首先是k==-1时,说明前面已无匹配状态,我们重新开始
            // 然后是p[j] == p[k],说明循环时新添加的值,与我们应该返回比对的位置相同
            // 同时由于我们之前的部分都是已经匹配成功的,所以加上这个数使我们的匹配长度又增加一位
            if (k == -1 || p[j] == p[k]) {

                // 当两个字符相等时要跳过
                //(因为p[k]与S[i]不符合的话,由于我们的p[j]=p[k],所以肯定也不符合,我们直接跳下一步)
                if (p[++j] == p[++k]) {

                    next[j] = next[k];

                } else {
                    // 因为在P[j]之前已经有P[0 ~ k-1] == p[j-k ~ j-1]。(next[j] == k)
                    // 这时候现有P[k] == P[j],我们是不是可以得到P[0 ~ k-1] + P[k] == p[j-k ~ j-1] + P[j]。
                    // 即:P[0 ~ k] == P[j-k ~ j],即next[j+1] == k + 1 == next[j] + 1
                    // 前面我们已经进行了j++和k++,所以这里直接赋值即可
                    next[j] = k;

                }

            } else {
                // 如果当前状态无法匹配,我们就跳回上一个前缀后缀相同部分再来判断是否前后缀相同
                k = next[k];

            }

        }

        return next;

    }
}

以上就是Java数据结构之KMP算法的实现的详细内容,更多关于Java KMP算法的资料请关注我们其它相关文章!

(0)

相关推荐

  • Java 数据结构与算法系列精讲之KMP算法

    概述 从今天开始, 小白我将带大家开启 Java 数据结构 & 算法的新篇章. KMP 算法 KMP (Knuth-Morris-Pratt), 是一种改进的字符串匹配算法. KMP 算法解决了暴力匹配需要高频回退的问题, KMP 算法在匹配上若干字符后, 字符串位置不需要回退, 从而大大提高效率. 如图: 举个例子 (字符串 "abcabcdef" 匹配字符串 "abcdef"): 次数 暴力匹配 KMP 算法 说明 1 abcabcdef abcdef

  • 详解Java中KMP算法的图解与实现

    目录 图解 代码实现 图解 kmp算法跟之前讲的bm算法思想有一定的相似性.之前提到过,bm算法中有个好后缀的概念,而在kmp中有个好前缀的概念,什么是好前缀,我们先来看下面这个例子. 观察上面这个例子,已经匹配的abcde称为好前缀,a与之后的bcde都不匹配,所以没有必要再比一次,直接滑动到e之后即可. 那如果好前缀中有互相匹配的字符呢? 观察上面这个例子,这个时候如果我们直接滑到好前缀之后,则会过度滑动,错失匹配子串.那我们如何根据好前缀来进行合理滑动? 其实就是看当前的好前缀的前缀和后缀

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

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

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

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

  • 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数据结构之KMP算法的实现

    目录 问题介绍 暴力求解 知识补充 Next示例 Next代码 匹配示例 匹配代码 完整代码 本次我们介绍数据结构中的KMP算法,我们会从下面几个角度来介绍: 问题介绍 首先我们先介绍适用于KMP算法的问题: 给定一个字符串S,以及一个模式串P,所有字符串中只包含大小写英文字母以及阿拉伯数字. 模式串P在字符串S中多次作为子串出现. 求出模式串P在字符串S中所有出现的位置的起始下标. 我们给出一个问题的简单示例: // 输入 p长度 p s长度 s 3 aba 5 ababa // 输出结果 0

  • java几种排序算法的实现及简单分析

    本文实例讲述了java几种排序算法的实现及简单分析.分享给大家供大家参考.具体如下: package test; public class first { /*普通的插入排序*/ public void insertSort(int[] list) { int i, j; list[0] = -999; //相当于设置一个监视哨兵,不用判断是否越界, //但要求数组从第二个数开始即i=1开始存储 for (i = 1; i < list.length; i++) { j = i; while (

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

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

  • Java编程中快速排序算法的实现及相关算法优化

    时间复杂度 平均情况:O(nlgn) 最坏情况:O(n*n),发生在当数据已经是排序状态时 快排算法的基本原理 1.从数据中选取一个值a[i]作为参考 2.以a[i] 为参考,将数据分成2部分:P1.P2,P1中的数据全部≤a[i],P2中的数据全部>a[i],数据变为{{P1}{a[i]}{P2}} 3.将P1.P2重复上述步骤,直到各部分中只剩1个数据 4.数据完成升序排列 基本示例: 原始数据: {3,9,8,5,2,1,6} 第1步:选取第1个数据:3 第2步:将数据分成2部分,左边≤3

  • Java数据结构之AC自动机算法的实现

    目录 1 概念和原理 2 节点定义 3 构建Trie前缀树 4 构建fail失配指针 5 匹配文本 6 案例演示 7 总结 1 概念和原理 一般的字符串匹配算法都是匹配一个子串,例如KMP.Trie,那么如果同时匹配多个子串呢?此时就需要用到AC自动机了. AC自动机算法是一个多模式字符串匹配算法,在模式匹配领域被广泛应用,例如违禁词查找替换.搜索关键词查找等等. 关于Trie树和KMP算法,我们此前已经讲解过了: 前缀树Trie的实现原理以及Java代码的实现 KMP算法详解以及Java代码实

  • Java十大经典排序算法的实现图解

    目录 前言 一.排序算法 1.排序算法概述(百度百科) 2.<数据结构与算法>中的排序算法 3.算法分析 二.十大经典排序算法(Java开发版) 1.冒泡排序 2.快速排序 3.基数排序 4.插入排序 5.选择排序 6.希尔排序 7.归并排序 8.计数排序 9.堆排序 10.桶排序 前言 本文章主要是讲解我个人在学习Java开发环境的排序算法时做的一些准备,以及个人的心得体会,汇集成本篇文章,作为自己对排序算法理解的总结与笔记. 内容主要是关于十大经典排序算法的简介.原理.动静态图解和源码实现

  • Java 二分查找算法的实现

    二分查找又称折半查找,它是一种效率较高的查找方法. 折半查找的算法思想是将数列按有序化(递增或递减)排列,查找过程中采用跳跃式方式查找,即先以有序数列的中点位置为比较对象,如果要找的元素值小 于该中点元素,则将待查序列缩小为左半部分,否则为右半部分.通过一次比较,将查找区间缩小一半. 折半查找是一种高效的查找方法.它可以明显减少比较次数,提高查找效率.但是,折半查找的先决条件是查找表中的数据元素必须有序. 折半查找法的优点是比较次数少,查找速度快,平均性能好;其缺点是要求待查表为有序表,且插入删

  • Java数据结构之图(动力节点Java学院整理)

    1,摘要: 本文章主要讲解学习如何使用JAVA语言以邻接表的方式实现了数据结构---图(Graph).从数据的表示方法来说,有二种表示图的方式:一种是邻接矩阵,其实是一个二维数组:一种是邻接表,其实是一个顶点表,每个顶点又拥有一个边列表.下图是图的邻接表表示. 从图中可以看出,图的实现需要能够表示顶点表,能够表示边表.邻接表指是的哪部分呢?每个顶点都有一个邻接表,一个指定顶点的邻接表中,起始顶点表示边的起点,其他顶点表示边的终点.这样,就可以用邻接表来实现边的表示了.如顶点V0的邻接表如下: 与

随机推荐