字符串查找匹配算法

news2024/12/26 23:23:57

概述

字符串匹配(查找)是字符串的一种基本操作:给定带匹配查询的文本串S和目标子串T,T也叫做模式串。在文本S中找到一个和模式T相符的子字符串,并返回该子字符串在文本中的位置。

暴力匹配

Brute Force Algorithm,也叫朴素字符串匹配算法,Naive String Matching Algorithm。
基本思路就是将字符一个一个地进行比较:

  • 如果S和T两个字符串的第一个字符相同就比较第二个字符,如果相同就一直继续;
  • 如果其中有某一个字符不同,则将T字符串向后移一位,将S字符串的第二个字符与T的字符串的第一个字符重新开始比较。
  • 循环往复,一直到结束

实现

public static int bf(String text, String pattern) {
    int m = text.length();
    int n = pattern.length();
    for (int i = 0; i <= m - n; i++) {
        boolean flag = true;
        for (int j = 0; j < n; j++) {
            if (text.charAt(i + j) != pattern.charAt(j)) {
                flag = false;
                break;
            }
        }
        if (flag) {
            return i;
        }
    }
    return -1;
}

KMP

D.E.Knuth,J.H.Morris 和 V.R.Pratt发明,一种字符串匹配的改进算法,利用匹配失败后的信息,尽量减少模式串与主串的匹配次数以达到快速匹配的目的。KMP算法只需要对文本串搜索一次,时间复杂度是O(n)

KMP 算法的关键是求 next 数组。next 数组的长度为模式串的长度。next 数组中每个值代表模式串中当前字符前面的字符串中,有多大长度的相同前缀后缀。

原理

暴力匹配
给定字符串A和B,判断B是否是A的子串。暴力匹配的方法:从A的第一个字符开始,比较A的第一个字符和B的第一个字符是否相同,相同则比较A的第二个字符和B的第二个字符,不相同,则从A的第二个字符开始,与B的第一个字符开始比较。以此类推。相当于一步步右移B的动作。

暴力匹配,效率低,体现在一步步移动,尤其是在B的前n-1个字符都能匹配成功,最后一个字符匹配失败,或B子串较长的情况下。KMP利用匹配表的概念来优化匹配效率。

匹配表
对于给定的字符串B,如abcab

  • 前缀:除了最后个字符外,所有的顺序组合方式:a、ab、abc、abca
  • 后缀:除了第一个字符外,所有的顺序组合方式:bcab、cab、ab、b

匹配值,对子串的每个字符组合寻找出前缀和后缀,比较是否有相同的,相同的字符组合有几位,匹配值就是几。

比如针对给定的字符串abcab,其匹配值字符串为00012

  • a,无前后缀,匹配值=0
  • ab,前缀{a},后缀{b},无共同字符,匹配值=0
  • abc,前缀{a}{ab},后缀{c}{bc},无共同字符,匹配值=0
  • abca,前缀{a}{ab}{abc},后缀{a}{ca}{bca},共同字符{a},匹配值=1
  • abcab,前缀{a}{ab}{abc}{abca},后缀{b}{ab}{cab}{bcab},共同字符{ab},匹配值=2

基于匹配表,不需要一个个移动,移动步数 = 成功匹配的位数 - 匹配表里面的匹配值

实现

给出一个KMP算法实现:

/**
 * 利用KMP算法求解pattern是否在text中出现过
 *
 * @param text    文本串
 * @param pattern 模式串
 * @return pattern在text中出现,则返回true,否则返回false
 */
public static boolean kmpSearch(String text, String pattern) {
    // 部分匹配数组
    int[] partMatchTable = kmpNext(pattern);
    // text中的指针
    int i = 0;
    // pattern中的指针
    int j = 0;
    while (i < text.length()) {
        if (text.charAt(i) == pattern.charAt(j)) {
            // 字符匹配,则两个指针同时后移
            i++;
            j++;
        } else if (j > 0) {
            // 字符失配,则利用next数组,移动j指针,避免i指针回退
            j = partMatchTable[j - 1];
        } else {
            // pattern中的第一个字符就失配
            i++;
        }
        if (j == pattern.length()) {
            // 搜索成功
            return true;
        }
    }
    return false;
}

private static int[] kmpNext(String pattern) {
    int[] next = new int[pattern.length()];
    next[0] = 0;
    int j=0;
    for (int i = 1; i < pattern.length(); i++) {
        while (j > 0 && pattern.charAt(i) != pattern.charAt(j)){//前后缀相同
            j = next[j - 1];
        }
        if (pattern.charAt(i) == pattern.charAt(j)){//前后缀不相同
            j++;
        }
        next[i] = j;
    }
    return next;
}

Boyer-Moore

Boyer-Moore 算法在实际应用中比 KMP 算法效率高,据说各种文本编辑器的查找功能,包括Linux 里的 grep 命令,都是采用 Boyer-Moore 算法。该算法有坏字符好后缀两个概念,字符串从后往前匹配。 一般情况下,比KMP算法快3-5倍。

原理

假设文本串S长度为n,模式串T长度为m,BM算法的主要特征为:

  • 从右往左进行比较匹配(一般的字符串搜索算法如KMP都是从左往右进行匹配);
  • 算法分为两个阶段:预处理阶段和搜索阶段;
  • 预处理阶段时间和空间复杂度都是是O(m+),是字符集大小,一般为256;
  • 搜索阶段时间复杂度是O(mn)
  • 当模式串是非周期性的,在最坏的情况下算法需要进行3n次字符比较操作;
  • 算法在最好的情况下达到O(n/m),即只需要n/m次比较。

而BM算法在移动模式串的时候是从左到右,而进行比较的时候是从右到左的。

BM算法的精华就在于BM(text, pattern),BM算法当不匹配的时候一次性可以跳过不止一个字符。即它不需要对被搜索的字符串中的字符进行逐一比较,而会跳过其中某些部分。通常搜索关键字越长,算法速度越快。它的效率来自于这样的事实:对于每一次失败的匹配尝试,算法都能够使用这些信息来排除尽可能多的无法匹配的位置。即它充分利用待搜索字符串的一些特征,加快搜索的步骤。

BM算法包含两个并行的算法(也就是两个启发策略):坏字符算法(bad-character shift)和好后缀算法(good-suffix shift)。这两种算法的目的就是让模式串每次向右移动尽可能大的距离(即BM()尽可能大)。

实现

/**
 * Boyer-Moore算法是一种基于后缀匹配的模式串匹配算法,后缀匹配就是模式串从右到左开始比较,但模式串的移动还是从左到右的。
 * 字符串匹配的关键就是模式串的如何移动才是最高效的,Boyer-Moore为了做到这点定义了两个规则:坏字符规则和好后缀规则<br>
 * 坏字符规则<bR>
 * 1.如果坏字符没有出现在模式字符中,则直接将模式串移动到坏字符的下一个字符:<br>
 * 2.如果坏字符出现在模式串中,则将模式串最靠近好后缀的坏字符(当然这个实现就有点繁琐)与母串的坏字符对齐:<br>
 * 好后缀规则<bR>
 * 1.模式串中有子串匹配上好后缀,此时移动模式串,让该子串和好后缀对齐即可,如果超过一个子串匹配上好后缀,则选择最靠靠近好后缀的子串对齐。<br>
 * 2.模式串中没有子串匹配上后后缀,此时需要寻找模式串的一个最长前缀,并让该前缀等于好后缀的后缀,寻找到该前缀后,让该前缀和好后缀对齐即可。<br>
 * 3.模式串中没有子串匹配上后后缀,并且在模式串中找不到最长前缀,让该前缀等于好后缀的后缀。此时,直接移动模式到好后缀的下一个字符。<br>
 */
public static List<Integer> bmMatch(String text, String pattern) {
    List<Integer> matches = new ArrayList<>();
    int m = text.length();
    int n = pattern.length();
    // 生成模式字符串的坏字符移动结果
    Map<Character, Integer> rightMostIndexes = preprocessForBadCharacterShift(pattern);
    // 匹配的节点位置
    int alignedAt = 0;
    // 如果当前节点在可匹配范围内,即当前的A[k]必须在A[0, m-n-1)之间,否则没有必要做匹配
    while (alignedAt + (n - 1) < m) {
        // 循环模式组,查询模式组是否匹配 从模式串的最后面开始匹配,并逐渐往前匹配
        for (int indexInPattern = n - 1; indexInPattern >= 0; indexInPattern--) {
            // 1 定义待查询字符串中的当前匹配位置.
            int indexInText = alignedAt + indexInPattern;
            // 2 验证带查询字符串的当前位置是否已经超过最长字符,如果超过,则表示未查询到.
            if (indexInText >= m) {
                break;
            }
            // 3 获取到带查询字符串和模式字符串中对应的待匹配字符
            char x = text.charAt(indexInText);
            char y = pattern.charAt(indexInPattern);
            // 4 验证结果
            if (x != y) {
                // 4.1 如果两个字符串不相等,则寻找最坏字符串的结果,生成下次移动的队列位置
                Integer r = rightMostIndexes.get(x);
                if (r == null) {
                    alignedAt = indexInText + 1;
                } else {
                    // 当前坏字符串在模式串中存在,则将模式串最靠近好后缀的坏字符与母串的坏字符对齐,shift 实际为模式串总长度
                    int shift = indexInText - (alignedAt + r);
                    alignedAt += shift > 0 ? shift : 1;
                }
                // 退出匹配
                break;
            } else if (indexInPattern == 0) {
                // 4.2 匹配到的话 并且最终匹配到模式串第一个字符,便是已经找到匹配串,记录下当前的位置
                matches.add(alignedAt);
                alignedAt++;
            }
        }
    }
    return matches;
}

/**
 * 坏字符串
 * 依据待匹配的模式字符串生成一个坏字符串的移动列,该移动列中表明当一个坏字符串出现时,需要移动的位数
 */
private static Map<Character, Integer> preprocessForBadCharacterShift(String pattern) {
    Map<Character, Integer> map = new HashMap<>();
    for (int i = pattern.length() - 1; i >= 0; i--) {
        char c = pattern.charAt(i);
        if (!map.containsKey(c)) {
            map.put(c, i);
        }
    }
    return map;
}

参考:百度百科

Sunday

Daniel M.Sunday 于 1990 年提出的字符串模式匹配算法。其效率在匹配随机的字符串时比其他匹配算法更快。平均时间复杂度为O(n),最差情况的时间复杂度为O(n*m)

原理

Sunday 算法跟 KMP 算法一样,是从前往后匹配。在匹配失败时,关注文本串中参加匹配的最末位字符的下一位字符,如果该字符不在模式串中,则整个模式串移动到该字符之后。如果该字符在模式串中,将模式串右移使对应的字符对齐。

Sunday算法和BM算法稍有不同的是,Sunday算法是从前往后匹配,在匹配失败时关注的是主串中参加匹配的最末位字符的下一位字符。

  • 如果该字符没有在模式串中出现则直接跳过,即移动位数 = 模式串长度 + 1;
  • 否则,其移动位数 = 模式串长度 - 该字符最右出现的位置(以0开始) = 模式串中该字符最右出现的位置到尾部的距离 + 1。

举例说明Sunday算法。假定现在要在主串substring searching中查找模式串search

  • 刚开始时,把模式串与文主串左边对齐:
    在这里插入图片描述
  • 结果发现在第2个字符处发现不匹配,不匹配时关注主串中参加匹配的最末位字符的下一位字符,即标粗的字符i,模式串search中并不存在i,模式串直接跳过一大片,向右移动位数 = 匹配串长度 + 1 = 6 + 1 = 7,从 i 之后的那个字符(即字符n)开始下一步的匹配:
    在这里插入图片描述
  • 结果第一个字符就不匹配,再看主串中参加匹配的最末位字符的下一位字符r,它出现在模式串中的倒数第3位,于是把模式串向右移动3位(m - 3 = 6 - 3 = r 到模式串末尾的距离 + 1 = 2 + 1 =3),使两个r对齐:
    在这里插入图片描述
  • 匹配成功。

Sunday算法的缺点:算法核心依赖于move数组,而move数组的值则取决于模式串,那么就可能存在模式串构造出很差的move数组。

实现

/**
 * sunday 算法
 *
 * @param text    文本串
 * @param pattern 模式串
 * @return 匹配失败返回-1,匹配成功返回文本串的索引(从0开始)
 */
public static int sunday(char[] text, char[] pattern) {
    int tSize = text.length;
    int pSize = pattern.length;
    int[] move = new int[ASCII_SIZE];
    // 主串参与匹配最末位字符移动到该位需要移动的位数
    for (int i = 0; i < ASCII_SIZE; i++) {
        move[i] = pSize + 1;
    }
    for (int i = 0; i < pSize; i++) {
        move[pattern[i]] = pSize - i;
    }
    // 模式串头部在字符串位置
    int s = 0;
    // 模式串已经匹配的长度
    int j;
    // 到达末尾之前
    while (s <= tSize - pSize) {
        j = 0;
        while (text[s + j] == pattern[j]) {
            j++;
            if (j >= pSize) {
                return s;
            }
        }
        s += move[text[s + pSize]];
    }
    return -1;
}

对比

仅做示例用,不同的算法在不同情况下表现不一致。与待搜索的字符串文本和模式匹配字符串有关。

public static void main(String[] args) {
    String text = "abcagfacjkackeac";
    String pattern = "ackeac";
    Stopwatch stopwatch = Stopwatch.createStarted();
    int bfRes = bf(text, pattern);
    stopwatch.stop();
    log.info("bf result:{}, take {}ns", bfRes, stopwatch.elapsed(TimeUnit.NANOSECONDS));

    stopwatch.reset();
    stopwatch.start();
    boolean kmpRes = kmpSearch(text, pattern);
    stopwatch.stop();
    log.info("kmp result:{}, take {}ns", kmpRes, stopwatch.elapsed(TimeUnit.NANOSECONDS));

    stopwatch.reset();
    stopwatch.start();
    List<Integer> bmMatch = bmMatch(text, pattern);
    stopwatch.stop();
    log.info("bmMatch result:{}, take {}ns", bmMatch, stopwatch.elapsed(TimeUnit.NANOSECONDS));

    stopwatch.reset();
    stopwatch.start();
    int sunday = sunday(text.toCharArray(), pattern.toCharArray());
    stopwatch.stop();
    log.info("sunday result:{}, take {}ns", sunday, stopwatch.elapsed(TimeUnit.NANOSECONDS));
}

某次输出结果:

bf result:10, take 8833ns
kmp result:true, take 4541ns
bmMatch result:[10], take 90500ns
sunday result:10, take 5458ns

测试结果仅供参考。

参考

  • 动画解释KMP算法

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

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

相关文章

20天突破英语四级高频词汇——第②天

2&#xfeff;0天突破英语四级高频词汇~第2天加油(ง •_•)ง&#x1f4aa; &#x1f433;博主&#xff1a;命运之光 &#x1f308;专栏&#xff1a;英语四级高频词汇速记 &#x1f30c;博主的其他文章&#xff1a;点击进入博主的主页 目录 2&#xfeff;0天突破英语四级高…

LouvainMethod分布式运行的升级之路

1、背景介绍 Louvain是大规模图谱的谱聚类算法&#xff0c;引入模块度的概念分二阶段进行聚类&#xff0c;直到收敛为止。分布式的代码可以在如下网址进行下载。 GitHub - Sotera/spark-distributed-louvain-modularity: Spark / graphX implementation of the distri…

SQL server 与 MySQL count函数、以及sum、avg 是否包含 为null的值

sql server 与 mysql count 作用一样。 count 计算指定字段出现的个数&#xff0c; 不是计算 null的值 获取表的条数 count(n) n:常数 count(1),count&#xff08;0&#xff09;等 count(*) count(字段) 其中字段为null 不会统计在内。 avg(字段)、sum(字段) 跟count(字段)…

数字孪生技术的实用价值体现在哪?

随着科技的不断进步&#xff0c;数字孪生技术已成为引领未来发展的重要驱动力。数字孪生是将现实世界与数字世界紧密结合的技术&#xff0c;通过创建虚拟的物理模型&#xff0c;实时模拟和分析真实世界中的物体和过程&#xff0c;让数字孪生在各个领域都展现出了巨大的潜力&…

通用Mapper的四个常见注解

四个常见注解 1、Table 作用&#xff1a;建立实体类和数据库表之间的对应关系。 默认规则&#xff1a;实体类类名首字母小写作为表名&#xff0c;如 Employee -> employee 表 用法&#xff1a;在 Table 注解的 name 属性中指定目标数据库的表名&#xff1b; 案例&#…

vscode extension 怎么区分dev prod

开发模式注入环境变量 使用vsode 提供的api

【返回时间字段问题--消息转换器】

文章目录 前言一、第一种方式一、第二种方式&#xff08;推荐 ) 前言 一、第一种方式 1). 方式一 在属性上加上注解&#xff0c;对日期进行格式化 但这种方式&#xff0c;需要在每个时间属性上都要加上该注解&#xff0c;使用较麻烦&#xff0c;不能全局处理。 一、第二种方…

【数理知识】旋转矩阵的推导过程,基于向量的旋转来实现,同时解决欧式变换的非线性局限

序号内容1【数理知识】自由度 degree of freedom 及自由度的计算方法2【数理知识】刚体 rigid body 及刚体的运动3【数理知识】刚体基本运动&#xff0c;平动&#xff0c;转动4【数理知识】向量数乘&#xff0c;内积&#xff0c;外积&#xff0c;matlab代码实现5【数理知识】协…

WGS_1984_UTM、WGS_1984_Mercator坐标转化为经纬度坐标python

1、遥感影像的PROJECTION有哪些 遥感影像常见的投影类型有很多&#xff0c;具体选择哪种投影方式取决于数据的特性和使用需求。以下列举了一些常见的遥感影像投影类型&#xff1a; UTM (Universal Transverse Mercator) 投影&#xff1a;最常见的投影类型之一&#xff0c;将地…

[原创]从强化学习的本质推导到PPO

前言 这篇博客很久之前就想做了&#xff0c;一直在拖是因为觉得自己对知识点理解还没有足够的透彻。但是每当去复盘基本概念的时候又很难理清逻辑&#xff0c;所以觉得即便现在半吊子水平&#xff0c;但是也想通过博客记录一下自己肤浅的学习心得&#xff0c;权当是为自己巩固…

ParallelCollectionRDD [0] isEmpty at KyuubiSparkUtil.scala:48问题解决

ParallelCollectionRDD [0] isEmpty at KyuubiSparkUtil.scala:48问题解决 这个问题出现在使用Kyubi Spark Util处理ParallelCollectionRDD的过程中&#xff0c;具体是在KyubiSparkUtil.scala文件的第48行调用isEmpty方法时出现的。该问题可能是由以下几个原因引起的&#xff1…

C语言文件操作笔记

目录 1.文件 1.1 文件名 1.2 文件类型 1.3 文件缓冲区 1.4 文件指针 1.5 文件的打开和关闭 1.6 文件的顺序读写 1.6.1 fputc():向指定的文件中写入一个字符 1.6.2 fgetc():从指定的文件中读取一个字符 1.6.3 fgets():从指定的流 stream 读取一行 1.6.4 puts():向标准…

华为发布数字资产继承功能

在华为开发者大会2023&#xff08;HDC.Together&#xff09;上&#xff0c;华为常务董事、终端BG CEO、智能汽车解决方案BU CEO余承东正式发布了数字资产继承功能&#xff0c;HarmonyOS提供了安全便捷的数字资产继承路径。 在鸿蒙世界中&#xff0c;我们每个人在每台设备、应用…

开箱报告,Simulink Toolbox库模块使用指南(二)——MATLAB Fuction模块

文章目录 前言 MATLAB Fuction模块 采样点设置 FFT 求解 分析和应用 总结 前言 见《开箱报告&#xff0c;Simulink Toolbox库模块使用指南&#xff08;一&#xff09;——powergui模块》 MATLAB Fuction模块 MATLAB Fuction模块是在Simulink建模仿真或生成代码时&#x…

扩散模型实战(三):扩散模型的应用

推荐阅读列表&#xff1a; 扩散模型实战&#xff08;一&#xff09;&#xff1a;基本原理介绍 扩散模型实战&#xff08;二&#xff09;&#xff1a;扩散模型的发展 扩散只是一种思想&#xff0c;扩散模型也并非固定的深度网络结构。除此之外&#xff0c;如果将扩散的思想融入…

【并发编程】无锁环形队列Disruptor并发框架使用

Disruptor 是苹国外厂本易公司LMAX开发的一个高件能列&#xff0c;研发的初夷是解决内存队列的延识问顾在性能测试中发现竟然与10操作处于同样的数量级)&#xff0c;基于Disruptor开发的系统单线程能支撑每秒600万订单&#xff0c;2010年在QCn演讲后&#xff0c;获得了业界关注…

软件单元测试

单元测试目的和意义 对于非正式的软件&#xff08;其特点是功能比较少&#xff0c;后续也不有新特性加入&#xff0c;不用负责维护&#xff09;&#xff0c;我们可以使用debug单步执行&#xff0c;内存修改&#xff0c;检查对应的观测点是否符合要求来进行单元测试&#xff0c…

操作指南 | 如何使用Chainlink喂价功能获取价格数据

Chainlink的去中心化预言机网络中的智能合约包含由运行商为其他智能合约&#xff08;DApps&#xff09;使用或截取所持续更新的实施价格数据。其中有两个主要架构&#xff1a;喂价和基础要求模型。此教程将会展现如何在Moonbeam、Moonriver或是Moonbase Alpha测试网上使用喂价功…

使用一个python脚本抓取大量网站【1/3】

一、说明 您是否曾经想过抓取网站&#xff0c;但又不想为像Octoparse这样的抓取工具付费&#xff1f;或者&#xff0c;也许您只需要从网站上抓取几页&#xff0c;并且不想经历设置抓取脚本的麻烦。在这篇博文中&#xff0c;我将向您展示我如何创建一个工具&#xff0c;该工具能…

2023爱分析·信创云市场厂商评估报告:中国电子云

01 研究范围定义 信创2.0时代开启&#xff0c;信创进程正在从局部到全面、从细分到所有领域延展。在这个过程中&#xff0c;传统的系统集成,也在逐步向信创化、数字化及智能化转变。随着信创产业的发展&#xff0c;企业需要更多的技术支持和服务&#xff0c;而传统的系统集成已…