数据结构与算法学习总结(六)——字符串的模式匹配算法

2023-11-19

基本概念

字符串是一种特殊的线性表,即元素都是”字符“的线性表。

字符是组成字符串的基本单位,字符的取值依赖于字符集,例如二进制的字符集为0,1,则取值只能为(0,1),再比如英语语言,则包括26个字母外加标点符号。

例如"abcde"就是一个字符串,其中'a','b','c','d','e'都分别是串中的字符,串的长度是5。

线性表的存储结构同样适用于字符串,但是因为链式结构的额外开销,大部分情况下会采用顺序结构。

子串定义:串中任意个连续的字符组成的子序列称为该串的子串

例如:abc 就是 abcde的其中一个子串

特殊子串:

1.空字符串是任意串的子串

2.任意串S都是S本身的子串

3.真子串:非空且不为自身的子串(即不属于1,2两种情况下的)

 

关于字符串的其他运算不多做说明了,大多数人都太熟悉了,来看看难点的字符串的模式匹配以及实现算法吧。

模式匹配

模式匹配是数据结构中字符串的一种基本运算,给定一个串,要求在某个字符串中找出与该串相同的所有子串,这就是模式匹配

假设P是给定的子串,T是待查找的字符串,要求从T中找出与P相同的所有子串,这个问题成为模式匹配问题。P称为模式,T称为目标。如果T中存在一个或多个模式为P的子串,就给出该子串在T中的位置,称为匹配成功;否则匹配失败。

朴素模式匹配算法

假设T的长度为n,P的长度为m,比较容易想到的一个方法是初始时设置i=0&j=0,然后同时增大i与j判断n_i与m_j是否相等,直到j=m-1,如果一直相等,则匹配成功,如果n_i与m_j不相等,则设置i=i+1,j=0重新开始匹配,直到找到结果,或者i=n-m为止。

这种方式被称为BF算法(Brute Force),也被叫做朴素模式匹配算法或穷举法,代码实现如下(java):

package top.zhanglugao.patternmatch;

public class PatternMatch {

    public static void main(String[] args) {
        //目标串T
        String T="abacbcdhjk";
        //模式P
        String P="abad";
        String P2="cbcd";
        int i = bruteForcePatternMatch(P, T);
        System.out.println(i);
        i=bruteForcePatternMatch(P2,T);
        System.out.println(i);
        
        //运行结果分别是-1与3 程序无误
    }

    /***
     *
     * @param P     模式P
     * @param T     目标串T
     * @return
     */
    public static int bruteForcePatternMatch(String P,String T){
        int n=T.length();
        int m=P.length();
        //如果P的长度大于T 不可能查找成功返回-1
        if(n<m){
            return -1;
        }
        //i的最大值是n-m 这之后长度不够了
        for(int i=0;i<n-m;i++){
            //这里为了便于理解 采用了双层循环一个个字符比较的方式 实际上直接用T.substring(i,i+m)与P比较就可以了,里面这层嵌套没必要
            for(int j=0;j<m;j++){
                if(P.substring(j,j+1).equals(T.substring(i+j,i+j+1))){
                    if(j==m-1){
                        //匹配成功了
                        return i;
                    }
                    //相等 继续判断下个字符
                }else{
                    //匹配到不相等 可以跳出内层循环了
                    break;
                }
            }
        }
        return -1;
    }
}

下面来分析一下这个算法的复杂度:

匹配成功+最坏的情况下,如果直到最后一次才成功,那么i的值为(n-m)+1,j的值为m,总共比较次数为((n-m)+1)*m次,时间复杂度为O(n*m)。

匹配成功+最好的情况是T的前m个字符与P相等,总比较次数为m,时间复杂度为O(m)。

匹配失败+最坏的情况与匹配成功时相同。

匹配失败+最好的情况是每次比较第一个字符时就发现不相等,那么比较次数为(n-m)+1,时间复杂度为O(n)。

KMP算法(无回溯算法)

上面的穷举法是没有问题的,但不够好,如果是人为来寻找的时候,不会每次都将i回溯到i+1的位置。看下面比较的图。

在红色的位置出现了不匹配的情况,如果按照上面的朴素算法,会将i右移一位重新比较,但我们可能一眼就能看出来应该将i右移3位再比较,再之前的比较是无意义的,那么这其中有没有规律呢?我们尝试多观察一些同样的情况。

这个例子,我们显然应该右移两位,因为T中C前面的A与P开头的A是一致的。

这里应该右移三位,达到下面的状态:

仔细观察下可以发现这样的数学规律,如果存在一个常数k,使得T[i]之前k位与P开头的前k位相同,则可以设置j=k,继续比较。

也就是j要移动到的下一个位置k,存在最前面的k个字符与j之前的k个字符时相等的。即P[0]到P[k-1]==P[j-k]到P[j-1]

用图来表示的话就是下面这样:

其实也就是当匹配到P[j]!=T[i]时,其实意味着P位置j之前与T[i-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],这也是为什么可以跳过前面的部分直接时j=k继续比较。

接下来的问题是怎么求解k了,这样我们才能顺序编写程序来实现逻辑。

再根据前面的P[0 ~ k-1]==P[j-k ~ j-1],我们可以确定k的值只跟当前模式P以及当前j的值有关,而跟目标串T没有关系,而且对于每个位置j有确定的一个k。假设存储k的数组为next[]

            举个例子,当P=‘abaabcac’时,其各位置的next值计算过程为:

            a  j=0, 特殊情况,设置next(j)=-1
            b  j=1, 满足0<k<1的整数k不存在,next(j)=0
            a  j=2, 子串P1 != P0,k不存在,next(j)=0
            a  j=3, 存在且仅存在子串P2==P0,next(j)=k=1
            b  j=4, 存在且仅存在子串P3==P0,next(j)=k=1
            c  j=5, 存在最大子串P3P4==P0P1,next(j)=k=2
            a  j=6, 不存在要求子串,next(j)=0
            c  j=7, 存在且仅存在子串P6==P0,next(j)=k=1

尝试用java写个最弱智版本的求next数组的办法,之后我们再看看能不能优化,代码如下:

    /***
     * 获得next数组 对于循环中的j 寻找P[0]到p[k-1]=p[j-k]=p[j-1]  如果不存在这样的k k=0  因为java中substring方法含头不含尾的特性 对于尾部要进行+1的特殊处理
     * @param P  模式P
     * @return
     */
    public static Integer[] getNextArray(String P) {
        Integer[] next = new Integer[P.length()];
        next[0]=-1;
        for (int j = 1; j < P.length(); j++) {
            for (int k = j-1; k >0; k--) {
                System.out.println("j="+j+"&k="+k+"&P(0)到P(k-1)="+P.substring(0, k-1+1)+"&P(j)到P(j - 1)="+P.substring(j - k, j - 1+1));
                if (!P.substring(0, k-1+1).equals("")&&P.substring(0, k-1+1).equals(P.substring(j - k, j - 1+1))) {
                    next[j] = k;
                    System.out.println("j="+j+"&k="+k);
                    break;
                }
            }
            if (next[j] == null) {
                //没查找到
                System.out.println("没查找到 j=" + j);
                next[j] = 0;
            }
        }
        return next;
    }

这个方法显然效率不能让人接受,我们继续寻找一下规律。还是对于P=‘abaabcac’时,

a  j=0, 特殊情况,设置next(j)=-1
            b  j=1, 满足0<k<1的整数k不存在,next(j)=0,P[next(j)]=a!=P[j]
            a  j=2, 子串P1 != P0,k不存在,next(j)=0,P[next(j)]=a=P[j]
            a  j=3, 存在且仅存在子串P2==P0,next(j)=k=1,P[next(j)]=b!=P[j]
            b  j=4, 存在且仅存在子串P3==P0,next(j)=k=1,P[next(j)]=b=P[j]
            c  j=5, 存在最大子串P3P4==P0P1,next(j)=k=2,P[next(j)]=a!=P[j]
            a  j=6, 不存在要求子串,next(j)=0,P[next(j)]=a=P[j]
            c  j=7, 存在且仅存在子串P6==P0,next(j)=k=1,P[next(j)]=b!=P[j]

用递推法可以发现的规律是(当然我这种凡人是发现不了的,书上说了我才能确认。T_T):

  • 当P[next(j)]=P[j]时,next(j+1)=next(j)+1。这是可以用公式证明的,因为在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=2时满足这个条件,可以推到出j=3时,next[3]=k+1=1。在j=4时再次满足条件,所以满足next[5]=k+1=2。同理next[7]=next[6]+1=1
  • 那么当P[next(j)]!=p[j]的时候呢,我们先看个图:,像上边的例子,我们已经不可能找到[ A,B,A,B ]这个最长的后缀串了,但我们还是可能找到[ A,B ]、[ B ]这样的前缀串的。所以这个过程像不像在定位[ A,B,A,C ]这个串,当C和主串不一样了(也就是k位置不一样了),那当然是把指针移动到next[k]啦,所以只需要执行k=next[k]

经过优化过的求next数组的代码就变成了:

public static Integer[] getNextArray2(String P) {
        char[] p=P.toCharArray();
        Integer[] next = new Integer[P.length()];
        next[0] = -1;
        int j = 0;
        int k = -1;
        while (j < P.length() - 1) {
            if (k == -1 || p[j] == p[k]) {
                next[++j] = ++k;
            } else {
                k = next[k];
            }
        }
        return next;
    }

下面就可以写KMP算法了,代码实现如下:

public static int KMP(String ts, String ps) {
        char[] t = ts.toCharArray();
        char[] p = ps.toCharArray();
        int i = 0; // 主串T的位置
        int j = 0; // 模式串P的位置
        Integer[] next = getNextArray2(ps);
        while (i < t.length && j < p.length) {
            if (j == -1 || t[i] == p[j]) { // 当j为-1时,要移动的是i,当然j也要归0
                i++;
                j++;
            } else {
                // i不需要回溯了
                // i = i - j + 1;
                j = next[j]; // j回到指定位置
            }
        }
        if (j == p.length) {
            return i - j;
        } else {
            return -1;
        }
    }

算法分析:

由于j=next[j],每执行一次必然使得j减少,而使得j增加的操作只有j++;那么如果j=next[j]执行次数如果超过n(T的长度)次就会变成负数了。所以时间效率为O(n),同理求Next数组时间复杂度为O(m),所以KMP算法的时间复杂度为O(n+m),对比朴素的O(m*n)提升很大了。

 

ps:花了一整天,哎。CSDN针对我啊,每次都要审半天。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

数据结构与算法学习总结(六)——字符串的模式匹配算法 的相关文章

  • vsCode开发STM32设置头文件宏定义

    一 问题描述 使用 HAL 库新建 STM32 工程后 使用 vsCode 打开工程文件夹 会提示找不到头文件 以及比变量没有定义 如 uint32 t 不是一个有效类型等错误提示 如下图所示 二 原因分析 vsCode 中没有配置头文件路

随机推荐

  • 通过KXTF9-2050芯片分析I2C协议

    1 I2C协议 参见博客 I2C通信协议详解和通信流程分析 2 I2C驱动的框架分析 1 驱动框架分为两层 物理层和协议层 物理层是通用的 取决于主设备 协议层则每个从设备都不同 2 物理层 物理层可以理解成通用层 就是上面的介绍的I2C协
  • 嵌入式毕设分享 - stm32单片机酒精浓度酒驾检测系统 - 物联网 嵌入式

    文章目录 0 前言 1 简介 2 主要器件 3 实现效果 4 硬件设计 MQ 3酒精乙醇传感器模块 SIM800C模块 5 软件说明 系统框图 6 部分核心代码 7 最后 0 前言 这两年开始毕业设计和毕业答辩的要求和难度不断提升 传统的毕
  • 腾然教育MCN覃小龙公子:覃宣量2022年2岁10个月亲子照

    2022年8月3日 我和爱人 还有公子覃宣量 一同前往之前媳妇定好的拍摄店 叫做在红光桥下面的那个店 专门是儿童摄影的 在柳州做了好多年的 我们一家三口 一大早 就过去拍照了 下面这组我3岁啦 儿童摄影师非常有创意 直接让儿子每一个字拍一个
  • 【Random库】

    文章目录 random库概述 random库解析 random库概述 随机数在计算机应用中十分常见 Python内置的random库主要用于产生各种分布的伪随机数序列 random库采用梅森旋转算法 Mersenne twister 生成伪
  • 电脑出现msvcp120.dll丢失的解决方法,教你三招快速解决

    msvcp120 dll丢失是一件很常见的问题 出现msvcp120 dll丢失会导致电脑无法在正常运行 那么应该怎么解决这个问题呢 有什么办法可以快速的解决呢 今天教你三招快速解决msvcp120 dll丢失的方法 一 msvcp120
  • 时序预测

    时序预测 MATLAB实现CNN SVM卷积支持向量机时间序列预测 目录 时序预测 MATLAB实现CNN SVM卷积支持向量机时间序列预测 预测效果 基本介绍 研究回顾 程序设计 参考资料 预测效果 基本介绍 CNN SVM预测模型将深度
  • iphone投屏ipad_教你手机投屏电脑

    最近有很多小伙伴一直留言需要投屏软件 今天果子就来讲解一下关于投屏的问题 如果大家家里或者身边有类似天猫盒子这种的设备都是可以直接利用苹果自带的投屏服务AirPlay 屏幕镜像 进行投屏至电视 而我们的电脑分为USB投屏和无线投屏 WIN1
  • Android获取手机信号强度汇总

    雪里香梅 先报春来早 宋 欧阳修 蝶恋花 如今的天气是越来略冷了 每每走在凛冽的寒风中 心里就一个想法 春 假 天 期 怎么还不到 不知道大家有没有同感 前两天要做一个获取手机信号的小程序 于是在网上搜索了很多 就找到两种方法 遗憾的是都没
  • 【编译】gcc make cmake Makefile CMakeList.txt 关系、使用

    文章目录 一 关系 二 gcc 2 1 编译过程 2 2 编译参数 2 3 静态库和动态库 1 后缀名 2 联系与区别 2 4 GDB 调试器 1 常用命令 三 make makefile 四 cmake cmakelist 4 1 语法特
  • Android常用的加密算法

    一 MD5 MD5可以说是最基本最常用的加密算法了 还依稀记得在校招面试的时候被问到过 MD5信息摘要算法 MD5 Message Digest Algorithm 算法能将任意大小 格式的文字或文件进行加密从而产生 128 bit 16
  • 提高企业开发效率的优质工具:快速开发平台

    现代企业管理软件的功能越来越复杂 随着新技术作为管理手段不断被引入到管理软件中 使得管理软件的开发的难度在逐年的增加 尤其是企业需要的很多的功能都是个性化的 这让企业管理软件的开发少则半年 多则1年以上 而且失败率非常高 即使采用敏捷开发方
  • 使用 django-bootstrap3 库

    使用 django bootstrap3 库 1 配置 下载 pip install django bootstrap3 settings配置 在install apps中加上 bootstrap3 2 使用 在html文件中使用 表单 写
  • 如何的keil试试调试中,看逻辑分析仪Logic viwer

    在调试过程中 可以使用keil自带的逻辑分析仪查看变量的试试信息 减少串口输出 提高部分cpu的效率 可以添加以下信息 1 gpio引脚 2 全局变量 全局静态变量 局部变量是不行的 然后 添加变量后 需要右键设置 如下 g u32tick
  • el-table表格可拖拽实现

    druggerTable js function global factory global BmTableDrag factory global window function global function BmTableDrag op
  • win10系统镜像下载及在VMware虚拟机上创建win10虚拟机

    文章目录 前言 一 下载win10系统 二 配置win10系统虚拟机 创建新的虚拟机 注意事项 前言 网上很多win10镜像资源都没法用 不是下载不了 就是用不了 尤其公众号还有一堆套路 浪费自己很多时间 无奈 干脆自己做一个镜像 肯定好用
  • 【华为OD机试】数字游戏(C++ Python Java)2023 B卷

    时间限制 C C 1秒 其他语言 2秒 空间限制 C C 262144K 其他语言524288K 64bit IO Format lld 题目描述 小明玩一个游戏 系统发1 n张牌 每张牌上有一个整数 第一张给小明 后n张按照发牌顺序排成连
  • 数字化转型系列主题:数字化建设总体规划蓝图

    本文转自 CIO之家 数字化转型应该是千人千面 因为每家企业的难点痛点不一样 所以每家企业的转型路径都不尽相同 数字化转型不是为了转型而转型 必须是围绕解决企业最大痛点 以它做切入点 回报才最快 投入产出比才最高 所以常想 数字化转型第一件
  • 于仕琪的人脸检测算法

    于仕琪的人脸检测算法 对Windows下的商业使用也免费 刚更新了一次算法 正面人脸检测的角度范围从 40 40 度提升到 60 60 度 检测角度变大但计算量不增加 多视角人脸检测速度提升2倍 速度对比 在同样的条件下OpenCV 47
  • // 计算出给定矩阵中主对角线元素的和

    一 题目 计算出给定矩阵中主对角线元素的和 二 代码 include
  • 数据结构与算法学习总结(六)——字符串的模式匹配算法

    基本概念 字符串是一种特殊的线性表 即元素都是 字符 的线性表 字符是组成字符串的基本单位 字符的取值依赖于字符集 例如二进制的字符集为0 1 则取值只能为 0 1 再比如英语语言 则包括26个字母外加标点符号 例如 abcde 就是一个字