我的编程空间,编程开发者的网络收藏夹
学习永远不晚

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

短信预约 -IT技能 免费直播动态提醒
省份

北京

  • 北京
  • 上海
  • 天津
  • 重庆
  • 河北
  • 山东
  • 辽宁
  • 黑龙江
  • 吉林
  • 甘肃
  • 青海
  • 河南
  • 江苏
  • 湖北
  • 湖南
  • 江西
  • 浙江
  • 广东
  • 云南
  • 福建
  • 海南
  • 山西
  • 四川
  • 陕西
  • 贵州
  • 安徽
  • 广西
  • 内蒙
  • 西藏
  • 新疆
  • 宁夏
  • 兵团
手机号立即预约

请填写图片验证码后获取短信验证码

看不清楚,换张图片

免费获取短信验证码

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

本次我们介绍数据结构中的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;
        }
    }
}

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



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手写实例



// 我们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 {

    
    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);
    }

    
    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;

        }

    }

    
    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算法的资料请关注编程网其它相关文章!

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

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

下载Word文档到电脑,方便收藏和打印~

下载Word文档

猜你喜欢

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

这篇文章主要为大家详细介绍了Java数据结构中KMP算法的原理与实现,文中的示例代码讲解详细,对我们学习Java有一定的帮助,需要的可以参考一下
2022-11-21

Java数据结构之KMP算法怎么实现

这篇文章主要讲解了“Java数据结构之KMP算法怎么实现”,文中的讲解内容简单清晰,易于学习与理解,下面请大家跟着小编的思路慢慢深入,一起来研究和学习“Java数据结构之KMP算法怎么实现”吧!暴力匹配算法(Brute-Force,BF)这
2023-07-04

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

KMP算法是一种改进的字符串匹配算法,核心是利用之前的匹配失败时留下的信息,选择最长匹配长度直接滑动,从而减少匹配次数。本文主要介绍了KMP算法的原理与实现,需要的可以参考一下
2022-12-08

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

AC自动机算法常被认为是Trie树+KMP算法的结合体,它是一个多模式匹配算法,在模式匹配领域被广泛应用。本文将详细为大家介绍AC自动机的原理与实现方法,感兴趣的可以了解一下
2022-12-08

Java数据结构之AC自动机算法如何实现

本篇内容主要讲解“Java数据结构之AC自动机算法如何实现”,感兴趣的朋友不妨来看看。本文介绍的方法操作简单快捷,实用性强。下面就让小编来带大家学习“Java数据结构之AC自动机算法如何实现”吧!1 概念和原理一般的字符串匹配算法都是匹配一
2023-07-04

Java数据结构之常见排序算法怎么实现

这篇文章主要介绍“Java数据结构之常见排序算法怎么实现”的相关知识,小编通过实际案例向大家展示操作过程,操作方法简单快捷,实用性强,希望这篇“Java数据结构之常见排序算法怎么实现”文章能帮助大家解决问题。注:后续所说的复杂度 log,都
2023-07-04

Java数据结构之选择排序算法的实现与优化

选择排序:(Selection sort)是一种简单直观的排序算法,也是一种不稳定的排序方法。本文主要为大家介绍一下选择排序的实现与优化,希望对大家有所帮助
2023-01-28

java数据结构与算法之桶排序实现方法详解

本文实例讲述了java数据结构与算法之桶排序实现方法。分享给大家供大家参考,具体如下:基本思想:假定输入是由一个随机过程产生的[0, M)区间上均匀分布的实数。将区间[0, M)划分为n个大小相等的子区间(桶),将n个输入元素分配到这些桶中
2023-05-31

Java数据结构之栈与综合计算器的实现

这篇文章主要为大家详细介绍了Java数据结构中栈与综合计算器的实现,文中的示例代码讲解详细,具有一定的学习价值,感兴趣的小伙伴可以了解一下
2022-11-13

Java数据结构之双向链表的实现

相较单链表,双向链表除了data与next域,还多了一个pre域用于表示每个节点的前一个元素。这样做给双向链表带来了很多优势。本文主要介绍了双向链表的实现,需要的可以参考一下
2022-11-13

编程热搜

  • Python 学习之路 - Python
    一、安装Python34Windows在Python官网(https://www.python.org/downloads/)下载安装包并安装。Python的默认安装路径是:C:\Python34配置环境变量:【右键计算机】--》【属性】-
    Python 学习之路 - Python
  • chatgpt的中文全称是什么
    chatgpt的中文全称是生成型预训练变换模型。ChatGPT是什么ChatGPT是美国人工智能研究实验室OpenAI开发的一种全新聊天机器人模型,它能够通过学习和理解人类的语言来进行对话,还能根据聊天的上下文进行互动,并协助人类完成一系列
    chatgpt的中文全称是什么
  • C/C++中extern函数使用详解
  • C/C++可变参数的使用
    可变参数的使用方法远远不止以下几种,不过在C,C++中使用可变参数时要小心,在使用printf()等函数时传入的参数个数一定不能比前面的格式化字符串中的’%’符号个数少,否则会产生访问越界,运气不好的话还会导致程序崩溃
    C/C++可变参数的使用
  • css样式文件该放在哪里
  • php中数组下标必须是连续的吗
  • Python 3 教程
    Python 3 教程 Python 的 3.0 版本,常被称为 Python 3000,或简称 Py3k。相对于 Python 的早期版本,这是一个较大的升级。为了不带入过多的累赘,Python 3.0 在设计的时候没有考虑向下兼容。 Python
    Python 3 教程
  • Python pip包管理
    一、前言    在Python中, 安装第三方模块是通过 setuptools 这个工具完成的。 Python有两个封装了 setuptools的包管理工具: easy_install  和  pip , 目前官方推荐使用 pip。    
    Python pip包管理
  • ubuntu如何重新编译内核
  • 改善Java代码之慎用java动态编译

目录