34.KMP算法,拒绝暴力美学

news2025/1/11 12:51:46

概述

今天我们来聊一聊字符串匹配的问题。

比如有字符串str1 = “豫章故那,洪都新府。星分翼轸,地接衡庐。襟三江而带五湖,控蛮荆而引瓯越。”,字符串str2 = “襟三江而带五湖”。
现要判断str1是否含有str2,如果有则的返回第一次出现的位置,如果没有返回-1。

面对字符串匹配问题,我们通常都会直观的选择暴力匹配算法来解决,这固然能够满足业务要求但不可否认,它效率极低。

暴力匹配算法

如果用暴力匹配的思路,并假设现在str1匹配到 i 位置,子串str2匹配到 j 位置,则:

  1. 如果当前字符匹配成功(即str1 [ i ] == str2 [ j ] ) ,则i++,j++,继续匹配下一个字符;
  2. 如果失配(即str1 [ i ] != str2 [ j ]) ,令i=i - ( j - 1 ),j = 0。相当于每次匹配失败时,i 回溯,j 被置为0;
  3. 用暴力方法解决的话就会有大量的回溯,每次只移动一位,若是不匹配,移动到下一位接着判断,浪费了大量的时间。

代码实现

虽然暴力匹配算法效率极低,面临实际业务时基本不会采用这种方式,但是我还是将思路落实成代码,也方便与下文的KMP算法作比较。

public class ViolenceMatch {
    public static void main(String[] args) {
        String str1 = "豫章故那,洪都新府。星分翼轸,地接衡庐。襟三江而带五湖,控蛮荆而引瓯越。";
        String str2 = "襟三江而带五湖";
        int index = violenceMatch(str1,str2);
        System.out.println("index="+index);
    }

    //暴力匹配算法
    public static int violenceMatch(String str1,String str2){
        char[] s1 =  str1.toCharArray();
        char[] s2 =  str2.toCharArray();

        int s1Len = s1.length;
        int s2Len = s2.length;

        int i = 0;  //i索引指向s1
        int j = 0;  //j索引指向s2

        while (i<s1Len&&j<s2Len){//保证匹配时不越界
            if (s1[i]==s2[j]){
                i++;
                j++;
            }else {//没有匹配成功
                //失配(即str1[i]!=str2[j]),令i = i-(j-1), j=0。 即让i回到最初位置的后一位
                i = i - (j-1);
                j = 0;
            }
        }

        //判断是否匹配成功
        if (j==s2Len){
            return i-j;
        }else {
            return -1;
        }
    }
}

看似时间复杂度不高,O(n)似乎可以接受,但实际上,循环中的回溯

	i = i - (j-1);
    j = 0;

真正运行起来是天大的灾难,str1长度越长,效率越低。
解决字符串匹配问题还是采用KMP算法更合适一些。

KMP算法

  1. KMP是一个解决 求模式串在文本串是否出现过,如果出现过,求最早出现的位置的经典算法;
  2. Knuth-Morris-Pratt 字符串查找算法,简称为KMP算法,常用于在一个文本串S内查找一个模式串P 的出现位置,这个算法由Donald Knuth、Vaughan Pratt、James H.Morris三人于1977年联合发表,故取这3人的姓氏命名此算法
  3. KMP算法就利用之前判断过信息,通过一个next数组,保存模式串中前后最长公共子序列的长度,每次回溯时,通过next数组找到,前面匹配过的位置,省去了大量的计算时间。

思路分析

举例来说,有一个字符串 str1 = “BBCABCDAB ABCDABCDABDE”,判断,里面是否包含另一个字符串str2=“ABCDABD”。

  1. 首先,用 str1 的第一个字符和 str2 的第一个字符去比较,不符合,关键词向后移动一位
    在这里插入图片描述
  2. 重复第一步,依旧不符合,继续后移
    在这里插入图片描述
  3. 持续匹配,直到出现匹配字符
    在这里插入图片描述
  4. 接着匹配字符串与模式串的下一个字符
    在这里插入图片描述
  5. 直到遇到二者不匹配的位置
    在这里插入图片描述
  6. 这时候,大概率会想到继续遍历 str1 的下一个字符,重复第 1步。(其实是很不明智的,因为此时 BCD 已经比较过了,没有必要再做重复的工作,一个基本事实是,当空格与 D 不匹配时,你其实知道前面六个字符是”ABCDAB”。KMP 算法的想法是,设法利用这个已知信息,不要把”搜索位置”移回已经比较过的位置,继续把它向后移,这样就提高了效率。)
    这里需要注意,所谓的“BCD已经比较过了,没必要做重复的工作”指的是:在已知ABCD互不匹配的前提下,我们此轮已经将str2的前几位ABCD…与str1的部分字符匹配上了,可惜的是后面没有匹配上,那么我们可以认为,从str2 的A 开始的这些匹配字段(ABCD)肯定不会再与A匹配了(因为ABCD互不匹配而后面的与BCD匹配,所以不可能再与A匹配)。
    上面的关键在于如何确定A与BCD不同。
  7. 那么问题来了,怎么将刚刚重复的步骤省略掉呢?不妨对str2计算一张部分匹配表
    在这里插入图片描述
  8. 已知空格与 D不匹配时,前面六个字符”ABCDAB”是匹配的。查表可知,最后一个匹配字符 B 对应的”部分匹配值”为 2,因此按照下面的公式算出向后移动的位数:
    移动位数 = 已匹配的字符数 - 对应的部分匹配值,因为 6-2 等于4,所以将搜索向后移动 4 位。
  9. 因为空格与C不匹配,搜索词还要继续往后移。这时,已匹配的字符数为 2(”AB”),对应的”部分匹配值”为 0。所以,移动位数 = 2-0,结果为 2,于是将搜索词向后移 2 位。
    在这里插入图片描述
    部分匹配值:A=0, AB=0, ABC=0, ABCD=0, ABCDA=1, ABCDAB=2
  10. 因为空格与A不匹配,继续后移一位
    在这里插入图片描述
  11. 逐位比较,直到发现c与D不匹配。于是,移动位数 = 6-2,继续将搜索词向后移动 4 位。
    在这里插入图片描述
  12. 逐位比较,直到搜索词的最后一位,发现完全匹配,于是搜索完成。如果还要继续搜索(即找出全部匹配),移动位数 =7-0,再将搜索词向后移动 7 位,这里就不再重复了。
    在这里插入图片描述

部分匹配表的产生

经过上面的思路拆解,相信大家都感受到了KMP算法的精妙,但是有一个很关键的问题,部分匹配表是怎么来的?
要理解这个问题,首先要说以下前缀、后缀的概念
比如对字符串:bread
前缀: b br bre brea
后缀: d ad ead read
“部分匹配值”就是”前缀”和”后缀”的最长的共有元素的长度。以 ABCDABD为例,

  • A的前缀和后缀都为空集,共有元素的长度为 0;
  • AB的前缀为[A],后缀为[B],共有元素的长度为 0;
  • ABC的前缀为[A,AB],后缀为[BC,C],共有元素的长度 0;
  • ABCD的前缀为[A,AB,ABC],后缀为[BCD,CD,D],共有元素的长度为 0;
  • ABCDA的前缀为[A,AB,ABC,ABCD],后缀为[BCDA, CDA,DA,A],共有元素为”A”,长度为1;
  • ABCDAB的前缀为[A,AB,ABC,ABCD,ABCDA],后缀为[BCDAB,CDAB,DAB,AB,B],共有元素为”AB”
    长度为 2;
  • ABCDABD的前缀为[A,AB,ABC,ABCD,ABCDA,ABCDAB], 后缀为[BCDABD,CDABD,DABD,ABD,BD,D].共有元素的长度为 0。

也就是说

• 部分匹配值是“前缀”数组和“后缀”数组中最长的相同元素的长度;
• 部分匹配表中左起第一个A表示字符串为 “A”时的部分匹配值为0, 即前后缀无共有元素;
• 部分匹配表中左起第一个D表示字符串为 “ABCD”时的部分匹配值为0, 即前后缀无共有元素;
• 部分匹配表中左起第二个A表示字符串为 “ABCDA”时的部分匹配值为1, 即前后缀最长共有元素"A";
• 部分匹配表中左起第二个B表示字符串为 “ABCDAB”时的部分匹配值为2, 即前后缀最长共有元素"AB";
• 部分匹配表中左起第二个D表示字符串为 “ABCDABD”时的部分匹配值为0, 即前后缀无共有元素;

KMP代码实现

KMP方案

  1. 先得到字串的部分匹配表
  2. 使用部分匹配表完成KMP匹配

实现

public class KMPAlgorithm {
    public static void main(String[] args) {
        String str1 = "BBC ABCDAB ABCDABCDABDE";
        String str2 = "ABCDABD";
        int [] next = kmpNext(str2);
        System.out.println("next="+Arrays.toString(next));
        int inx = kmpSearch(str1, str2, next);
        System.out.println("index="+inx);
    }

    /**
     * kmp搜索算法
     * @param str1 源字符串
     * @param str2 字串
     * @param next 字串对应的 部分匹配表
     * @return -1即无匹配,否则返回第一个匹配的位置
     */
    public static int kmpSearch(String str1,String str2,int [] next){

        //遍历,i在源字串,j在子串
        for (int i = 0,j=0; i < str1.length(); i++) {
            //需要处理str1.charAt(i)!=str2.charAt(j)不相等,调整j的大小
            //kmp算法核心
            while (j>0&&str1.charAt(i)!=str2.charAt(j)){
                j = next[j-1];
            }

            //str1.charAt(i)==str2.charAt(j)相等
            if(str1.charAt(i)==str2.charAt(j)){
                j++;
            }

            if (j==str2.length())
                return i-j+1;//找到了
        }
        return -1;

    }

    /**
     * 获取到一个字符串(子串)的部分匹配值(表)
     * @param dest 匹配子串
     * @return 部分匹配值(表)对应数组
     */
    public static int[] kmpNext(String dest){
        //创建一个next数组保存部分匹配值
        int [] next = new int[dest.length()];
        next[0] = 0;//如果字符串长度为1,则其部分匹配值必为0
        for (int i=1,j=0;i<dest.length();i++){
            //当dest.charAt(i)!=dest.charAt(j)时,我们需要从next[j-1]获取新的j
            //直到发现有dest.charAt(i)==dest.charAt(j)成立是才退出
            //这是kmp算法的核心点
            while (j>0&&dest.charAt(i)!=dest.charAt(j)){
                j = next[j-1];
//                j--;//?  类似于j--,但不知道为什么不能用
            }

            //当dest.charAt(i)==dest.charAt(j),满足时,部分匹配值就是+1
            if (dest.charAt(i)==dest.charAt(j)){
                j++;
            }
            next[i] = j;
        }
        return next;
    }
}

KMP代码的核心在于:

while (j>0&&dest.charAt(i)!=dest.charAt(j)){
                j = next[j-1];
//                j--;//?  类似于j--,但不知道为什么不能用
}

这里类似于回溯,在两匹配字符相等时,i与j同时++;在两匹配字符不等时,i不动,j通过next数组进行回溯比较(这里类似于j–,但是并不能等价,底层还要探究)。

小结

对KMP算法的简单介绍就到这里了,感兴趣的同学可以就相关问题做更深入的研究。


关注我,共同进步,每周至少一更。——Wayne

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1020167.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

zabbix介绍及部署(五十一)

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 目录 一、zabbix的基本概述 二、zabbix的构成 1、Server 2、web页面 3、数据库 4、proxy 5、Agent 三、zabbix的监控对象 四、zabbix的常用术语 五、zabbix的工作流程 六、za…

区域气象-大气化学在线耦合模式(WRF/Chem)在大气环境领域实践技术应用

大气污染是工农业生产、生活、交通、城市化等方面人为活动的综合结果&#xff0c;同时气象因素是控制大气污染的关键自然因素。大气污染问题既是局部、当地的&#xff0c;也是区域的&#xff0c;甚至是全球的。本地的污染物排放除了对当地造成严重影响外&#xff0c;同时还会在…

基于docker进行Grafana + prometheus实现服务监听

基于docker进行Grafana Prometheus实现服务监听 Grafana安装Prometheus安装Jvm监控配置 Grafana安装 docker pull grafana/grafanamkdir /server/grafanachmod 777 /server/grafanadocker run -d -p 3000:3000 --namegrafana -v /server/grafana:/var/lib/grafana grafana/gr…

Databend 开源周报第 111 期

Databend 是一款现代云数仓。专为弹性和高效设计&#xff0c;为您的大规模分析需求保驾护航。自由且开源。即刻体验云服务&#xff1a;https://app.databend.cn 。 Whats On In Databend 探索 Databend 本周新进展&#xff0c;遇到更贴近你心意的 Databend 。 理解 SHARE END…

9、DVWA——XSS(Stored)

文章目录 一、存储型XSS概述二、low2.1 源码分析2.2 通关分析 三、medium3.1 源码分析3.2 通关思路 四、high4.1 源码分析4.2 通关思路 一、存储型XSS概述 XSS&#xff0c;全称Cross Site Scripting&#xff0c;即跨站脚本攻击&#xff0c;某种意义上也是一种注入攻击&#xff…

MATLAB中filloutliers函数用法

目录 语法 说明 示例 在向量中对离群值进行插值 使用均值检测和最邻近值填充方法 使用移窗检测法 填充矩阵行中的离群值 指定离群值位置 返回离群值阈值 filloutliers函数功能是检测并替换数据中的离群值。 语法 B filloutliers(A,fillmethod) B filloutliers(A,f…

Paper Reading: RSPrompter,基于视觉基础模型的遥感实例分割提示学习

目录 简介目标工作重点方法实验总结 简介 题目&#xff1a;《RSPrompter: Learning to Prompt for Remote Sensing Instance Segmentation based on Visual Foundation Model 》&#xff0c;基于视觉基础模型的遥感实例分割提示学习 日期&#xff1a;2023.6.28 单位&#xf…

接口测试学习

1、curl 命令 无参&#xff1a;curl -X POST -H"Authorization: abcdefghijklmn" https://xxx.xxxxx.com/xxxx 有参&#xff1a;curl -X POST -H"Authorization:abcdefghijklmn " -H"Content-Type:application/json" https://xxx.xxxxx.com/…

synchronized锁详解

本文主要是对synchronized使用各个情况&#xff0c;加解锁底层原理的讲解 一&#xff0c;重量级锁 对象头 讲重量级锁之前&#xff0c;先了解一下一个对象的构成&#xff0c;一个对象是由对象头和对象体组成的&#xff0c;本文主要讲对象头&#xff0c;对象体其实就是对象的…

核心实验21_BGP高级(了解)(配置略)_ENSP

项目场景&#xff1a; 核心实验21_BGP基础_ENSP 通过bgp实现省市互通。 实搭拓扑图&#xff1a; 具体操作&#xff1a; 其他基础配置略&#xff08;接口地址&#xff0c;ospf&#xff09; 1.BGP邻居建立&#xff1a; R1: [R1]bgp 200 [R1-bgp]peer 10.2.2.2 as-number 200 …

Java高级之File类、节点流、缓冲流、转换流、标准I/O流、打印流、数据流

第13章 IO流 文章目录 一、File类的使用1.1、如何创建File类的实例1.2、常用方法1.2.1、File类的获取功能1.2.2、File类的重命名功能1.2.3、File类的判断功能1.2.4、File类的创建功能1.2.5、File类的删除功能 二、IO流原理及流的分类2.1、Java IO原理2.2、流的分类/体系结构 三…

LINUX内核启动流程-2

向32位模式转变,为main函数的调用做准备 1、关中断并将system移动到内存地址起始位置0x00000 1.1 关中断:将CPU的标志寄存器(EFLAGS)中的中断允许标志(IF)置0。 main函数中能够适应保护模式的中断服务体系被重建完毕才会打开中断,而那时候响应中断的服务程序将不再是…

【数据结构与算法】不就是数据结构

前言 嗨喽小伙伴们你们好呀&#xff0c;好久不见了,我已经好久没更新博文了&#xff01;之前因为实习没有时间去写博文&#xff0c;现在已经回归校园了。我看了本学期的课程中有数据结构这门课程&#xff08;这么课程特别重要&#xff09;&#xff0c;因为之前学过一点&#xf…

天宇微纳芯片测试软件如何测试电源芯片的持续电流?

持续电流&#xff08;连续电流&#xff09;是指元器件在工作状态下内部电流持续流动的状态&#xff0c;一般都是用于对元器件允许连续通过电流限制的一种描述。比如电源芯片允许的持续电流&#xff0c;就表示该芯片可连续通过的最大电流。 通过上面的描述我们可以知道&#xff…

爬虫 — 验证码反爬

目录 一、超级鹰二、图片验证模拟登录1、页面分析1.1、模拟用户正常登录流程1.2、识别图片里面的文字 2、代码实现 三、滑块模拟登录1、页面分析2、代码实现&#xff08;通过对比像素获取缺口位置&#xff09; 四、openCV1、简介2、代码3、案例 五、selenium 反爬六、百度智能云…

zabbix学习1--zabbix6.x单机

文章目录 1. 环境2. MYSQL8.02.1 单节点2.2 配置主从 3. 依赖组件4. zabbix-server5. agent5.1 yum5.2 编译 附录my.cnfJDK默认端口号 1. 环境 进入官网查看所需部署环境配置以及应用版本要求https://www.zabbix.com/documentation/current/zh/manual/installation/requiremen…

机器学习(11)---降维PCA

目录 一、概述1.1 维度1.2 sklearn中的降维算法 二、降维实现原理2.1 PCA与SVD2.2 降维实现2.3 降维过程 三、鸢尾花数据集降维3.1 高维数据的可视化3.2 探索降维后的数据3.3 累积可解释方差贡献率曲线 四、选n_components参数方法4.1 最大似然估计自选超参数4.2 按信息量占比选…

期权开户流程、交易时间和规则详解清晰易懂

本文将介绍期权开户流程、交易时间和规则详解清晰易懂则&#xff0c;包括期权的定义、期权交易的时间、期权交易的规则和期权交易的风险。本文的结论是&#xff0c;期权交易的时间和规则非常重要&#xff0c;应该遵守交易规则&#xff0c;并且要注意风险。本文来源&#xff1a;…

VB求组合数

VB求组合数 求组合数C(m,n)n!/(m!(n-m)!) m6,n10 Private Function fact(x As Integer) As LongDim i As Integer, f As Longf 1For i 1 To xf f * iNext ifact f End Function Private Sub Command1_Click()Dim m%, n%, u As Long, v As Long, w As Longm 6: n 10u fa…