字符串匹配算法——JavaScript

article/2025/9/21 12:27:52

字符串匹配算法——javascript

文章目录

    • 字符串匹配算法——javascript
    • 字符串匹配
      • BF算法 (暴力匹配) √
      • KMP算法 √
      • BM算法
        • **坏字符规则**
        • 好后缀规则
      • Trid树(字典树)√

字符串匹配

字符串匹配问题的形式定义:

  • **文本(Text)**是一个长度为 n 的数组 T[1…n];
  • **模式(Pattern)**是一个长度为 m 且 m≤n 的数组 P[1…m];
  • T 和 P 中的元素都属于有限的字母表 Σ 表
  • 如果 0≤s≤n-m,并且 T[s+1…s+m] = P[1…m],即对 1≤j≤m,有 T[s+j] = P[j],则说模式 P 在文本 T 中出现且位移为 s,且称 s 是一个有效位移(Valid Shift)

比如上图中,目标是找出所有在文本 T = abcabaabcabac 中模式 P = abaa 的所有出现。该模式在此文本中仅出现一次,即在位移 s = 3 处,位移 s = 3 是有效位移。

BF算法 (暴力匹配) √

也叫朴素的字符串匹配算法。

是最常想到的,也是最好实现的,所以在简单情况下可以直接使用。

首先从原字符串最左端开始匹配子字符串,如果第一个字符与子字符串匹配,则继续看第二个字符与子字符串第二个字符是否匹配。。。如果不匹配,则找原字符串下一位与子字符串第一位相匹配,以此类推

let arr = 'hello world';let sunArr = 'or';function matchingStr (arr, sunArr) {for(let i = 0; i < arr.length - sunArr.length + 1; i ++) {//如任意个长度的查找3个长度的子串,大字符串的最后3-1个长度没必要比的let j = 0;//用来判断查找的长度while(j < sunArr.length) {//只要查找长度不大于子串长度就可以继续比if(arr[i + j] != sunArr[j]) break;//循环的过程中只要有一个字符不符合,就退出j ++;//下一位}if(j == sunArr.length) return i;//完全符合}reutrn -1;}console.log(matchingStr(arr,sunArr));

其实一个循环就可以解决:

function matchingAtr(arr, sunArr) {let i = 0, j = 0;//i表示arr匹配的元素位置,j为sunArr匹配的元素位置while(i < arr.length && j < sunArr.length) {if(arr[i] == sunArr[j]) {i ++;j ++;} else {i = i - j + 1;//因为要下一位,所以+1j = 0;}}if(j == sunArr.length) {return i - j;} else {return -1;}}console.log(matchingAtr('hello world', 'rld'));

KMP算法 √

下面,我用自己的语言,试图写一篇比较好懂的KMP算法解释。

img

首先,字符串"BBC ABCDAB ABCDABCDABDE"的第一个字符与搜索词"ABCDABD"的第一个字符,进行比较。因为B与A不匹配,所以搜索词后移一位。

img

因为B与A不匹配,搜索词再往后移。

img

就这样,直到字符串有一个字符,与搜索词的第一个字符相同为止。

img

接着比较字符串和搜索词的下一个字符,还是相同。

img

直到字符串有一个字符,与搜索词对应的字符不相同为止。

img

这时,最自然的反应是,将搜索词整个后移一位,再从头逐个比较。这样做虽然可行,但是效率很差,因为你要把"搜索位置"移到已经比较过的位置,重比一遍。

img

一个基本事实是,当空格与D不匹配时,你其实知道前面六个字符是"ABCDAB"。KMP算法的想法是,设法利用这个已知信息,不要把"搜索位置"移回已经比较过的位置,继续把它向后移,这样就提高了效率。

img

怎么做到这一点呢?可以针对搜索词,算出一张《部分匹配表》(Partial Match Table)。这张表是如何产生的,后面再介绍,这里只要会用就可以了。

已知空格与D不匹配时,前面六个字符"ABCDAB"是匹配的。查表可知,最后一个匹配字符B对应的"部分匹配值"为2,因此按照下面的公式算出向后移动的位数:

移动位数 = 已匹配的字符数 - 对应的部分匹配值

因为 6 - 2 等于4,所以将搜索词向后移动4位。

img

因为空格与C不匹配,搜索词还要继续往后移。这时,已匹配的字符数为2(“AB”),对应的"部分匹配值"为0。所以,移动位数 = 2 - 0,结果为 2,于是将搜索词向后移2位。

img

因为空格与A不匹配,继续后移一位。

img

逐位比较,直到发现C与D不匹配。于是,移动位数 = 6 - 2,继续将搜索词向后移动4位。

img

逐位比较,直到搜索词的最后一位,发现完全匹配,于是搜索完成。如果还要继续搜索(即找出全部匹配),移动位数 = 7 - 0,再将搜索词向后移动7位,这里就不再重复了。

img

下面介绍《部分匹配表》是如何产生的。

首先,要了解两个概念:“前缀"和"后缀”。 "前缀"指除了最后一个字符以外,一个字符串的全部头部组合;"后缀"指除了第一个字符以外,一个字符串的全部尾部组合。

img

"部分匹配值"就是"前缀"和"后缀"的最长的共有元素的长度。以"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。

自己的看法:

其实这样实现起来个人觉得会很吃力,我倒有一个简易一点的算法:

  • 新建一个“部分匹配值”数组pmv = [] (partial matching value);
  • 从第二个元素开始for遍历搜索词,看是否与第一个元素相等,如果不相等,则将pmv的此位置设为0,i++进入下一for循环;如果二者相等,则进入一个while循环,先将pmv的此位置值设为1,然后比较当前位置的下一位是否与第二位相等,如果相等则将pmv的此位置设为2,直到不相等,退出while循环,继续for循环。 for循环完成后将pmv返回即为所求!
function computePMV(str) {let pmv = [0];for(let i = 1; i < str.length;) {let j = 0, time = 1;if(str[i] != str[j]) {pmv[i] = 0;i ++;} else {while(str[i] == str[j]) {pmv[i] = time;i ++;j ++;time ++;}}}return pmv;}console.log(computePMV('abcdabd'));

img

"部分匹配"的实质是,有时候,字符串头部和尾部会有重复。比如,“ABCDAB"之中有两个"AB”,那么它的"部分匹配值"就是2("AB"的长度)。搜索词移动的时候,第一个"AB"向后移动4位(字符串长度-部分匹配值),就可以来到第二个"AB"的位置。

KMP算法的时间复杂度为:O(m+n)

实现KMP算法:

//求next[]
function computePMV(str) {let pmv = [0];for(let i = 1; i < str.length;) {let j = 0, time = 1;if(str[i] != str[j]) {pmv[i] = 0;i ++;} else {while(str[i] == str[j]) {pmv[i] = time;i ++;j ++;time ++;}}}return pmv;
}
let bigArr = 'bbc abcdab abcdabcdabde'
let smallArr = 'abcdabd';
function KMP(bigArr,smallArr) {let next = computePMV(smallArr);for(let i = 0, j = 0; i < bigArr.length; ) {if(bigArr[i] != smallArr[j] && j == 0) i ++;//j不等于0的时候i不能动while(bigArr[i] == smallArr[j]) {//如果元素相等则开始while循环判断后续i ++;j ++;}if(j == smallArr.length) return i - j;if(j != 0) j = next[j - 1];}return -1;
}
console.log(KMP(bigArr, smallArr));

BM算法

一般文本编辑器中的查找功能都是基于它实现的 。

基于它的原理,通常搜索关键字越长,算法速度越快。

它的效率来自这样一个事实:

  • 对于每一次失败的匹配尝试,算法都能够使用这些信息来排除尽可能多的无法匹配的位置

它是基于以下两个规则让模式串每次向右移动 尽可能大 的距离。

  • 坏字符规则(bad-character shift):当文本串中的某个字符跟模式串的某个字符不匹配时,我们称文本串中的这个失配字符为坏字符,此时模式串需要向右移动,移动的位数 = 坏字符在模式串中的位置 - 坏字符在模式串中最右出现的位置。此外,如果"坏字符"不包含在模式串之中,则最右出现位置为 -1。坏字符针对的是文本串。
  • 好后缀规则(good-suffix shift):当字符失配时,后移位数 = 好后缀在模式串中的位置 - 好后缀在模式串上一次出现的位置,且如果好后缀在模式串中没有再次出现,则为 -1。好后缀针对的是模式串。

坏字符规则

坏字符出现的时候有两种情况进行讨论。

1、模式串中没有出现了文本串中的那个坏字符,将模式串直接整体对齐到这个字符的后方,继续比较。

2、模式串中有对应的坏字符时,让模式串中 最靠右 的对应字符与坏字符相对。

这句话有一个关键词是 最靠右

思考一下为什么是 最靠右

坏字符规则实现:

好后缀规则

1、如果模式串中存在已经匹配成功的好后缀,则把目标串与好后缀对齐,然后从模式串的最尾元素开始往前匹配。

2、如果无法找到匹配好的后缀,找一个匹配的最长的前缀,让目标串与最长的前缀对齐(如果这个前缀存在的话)。模式串[m-s,m] = 模式串[0,s]

3、如果完全不存在和好后缀匹配的子串,则右移整个模式串。

Trid树(字典树)√

  • Trie树,也叫字典树、字母树、前缀树,它是一种树形结构。是一种专门处理字符串匹配的数据结构,用来解决在一组字符串的集合中快速查找某字符串
  • Trie树本质,利用字符串之间的公共前缀,将重复的前缀合在一起。

比如在 cod, code, cook, five, file, fat 这个字符串集合中查找某个字符串是否存在。如果每次查找都依次比较的话,效率会很低,但是用Trid树来解决就会更高效。

建树的过程中,将字符串的最后一个字符标记为橙色

然后查找的时候,查到最后要判断最后字符是否为橙色,如果不是橙色然后也查找到了,那就说明它只是一个单词的中间子串,所以结果为不存在此字符串。

通过上图,可以发现Trid树的三个特点

  • 根节点不包括字符,除根节点外,每一个节点都只包含一个字符;
  • 从根节点到某一节点,路径上字符连接起来,即为该节点的字符串;
  • 每个节点的所有子节点包含的字符都不相同。

Trie树的插入操作:

比如要插入新单词cook,就有下面几步:

  • 插入第一个字母 c,发现 root 节点下方存在子节点 c,则共享节点 c
  • 插入第二个字母 o,发现 c 节点下方存在子节点 o,则共享节点 o
  • 插入第三个字母 o,发现 o 节点下方不存在子节点 o,则创建子节点 o
  • 插入第三个字母 k,发现 o 节点下方不存在子节点 k,则创建子节点 k
  • 至此,单词 cook 中所有字母已被插入 Trie树 中,然后设置节点 k 中的标志位,

Trie树的删除操作:

  • 从根节点开始查找第一个字符h
  • 找到h子节点后,继续查找h的下一个子节点i
  • i是单词hi的标志位,将该标志位去掉
  • i节点是hi的叶子节点,将其删除
  • 删除后发现h节点为叶子节点,并且不是单词标志位,也将其删除
  • 这样就完成了hi单词的删除操作

Trie树的局限性

如前文所讲,Trie的核心思想是空间换时间,利用字符串的公共前缀来降低查询时间的开销以达到提高效率的目的。 假设字符的种数有m个,有若干个长度为n的字符串构成了一个 Trie树 ,则每个节点的出度为 m(即每个节点的可能子节点数量为m),Trie树 的高度为n。很明显我们浪费了大量的空间来存储字符,此时Trie树的最坏空间复杂度为O(m^n)。也正由于每个节点的出度为m,所以我们能够沿着树的一个个分支高效的向下逐个字符的查询,而不是遍历所有的字符串来查询,此时Trie树的最坏时间复杂度为O(n)。 这正是空间换时间的体现,也是利用公共前缀降低查询时间开销的体现。

实现Trie树:

		//Trie树let charArr = ['cod', 'code', 'cook', 'five', 'file', 'fat'];let patternStr = 'cod';//建Trie树function insertHash(tree, patternStr) {//向Tree树中插入字符// let hashHead = tree;let pLen = patternStr.length;let nowNode = tree;for(let i = 0; i < pLen; i ++) {let now = patternStr[i];//要比较的字符if(nowNode[now] == undefined) {//不存在此字符let  hash = {orange: false};nowNode[now] = hash;//新建一个hash给patternStr}if(i == pLen - 1) //如果是字符的最后一位,则变为橙色nowNode[now].orange = true;nowNode = nowNode[now];}}//查找模式串是否在字符集中function Trie(charArr,patternStr) {//根据charArr建树let TrieTree = {};//Trie树let len = charArr.length;for(let i = 0; i < len; i ++) {insertHash(TrieTree, charArr[i]);}// console.log(TrieTree);//哈哈哈我真是太棒了!!//根据Trie树查找模式串let nowNode = TrieTree;//当前比较的for(let i = 0; i < patternStr.length; i ++) {let now = patternStr[i];if(nowNode[now]  != undefined) //说明有这个字符nowNode = nowNode[now];else //没有这个字符则返回没找到,falsereturn false;}//循环完之后没有return,则说明Trie树中有此字符,但也有可能是其他字符的子串,所以要看字符最后一位的orange是否为trueif(nowNode.orange == false) return false;else return true;}console.log(Trie(charArr, patternStr));

http://chatgpt.dhexx.cn/article/I5XLSLnf.shtml

相关文章

数据结构与算法--字符串匹配算法

目录 概要 单模式与多模式的区别 单模式匹配算法 BF算法 概念 代码实现 时间复杂度 应用 RK算法 概念 代码实现 时间复杂度 应用 BM算法 概念 算法原理 代码实现 时间复杂度 应用 多模式匹配算法 Trie树 概念 Trie树的插入 Trie树的查找 代码实现 时间复杂度 应用 概要 字符…

简单高效的字符串匹配算法

Quick Search算法 算法简介 Quick Search算法属于Sunday算法的一种。Sunday算法由Daniel M Sunday在1990年提出。论文原文&#xff1a;A VERV FAST SU6STRINC SEARCH ALGORITHM 在论文中&#xff0c;作者提出了三个不同的算法&#xff1a;Quick Search算法、Maximal Shift算…

算法之字符串匹配

字符串匹配&#xff1a;设 S 和 T 是给定的两个串&#xff0c;在主串 S 中找到模式串 T 的过程称为字符串匹配&#xff0c;如果在主串 S 中找到模式串 T &#xff0c;则称匹配成功&#xff0c;函数返回 T 在 S 中首次出现的位置&#xff0c;否则匹配不成功&#xff0c;返回 -1。…

经典字符串匹配算法——KMP算法

KMP算法 KMP算法是一种高效的字符串匹配算法&#xff0c;在传统暴力遍历匹配的基础上做了一定的优化。 首先KMP算法的实现也是使用了回退思想&#xff0c;不过与暴力遍历不同&#xff0c;KMP的回退&#xff0c;是让子串进行匹配&#xff0c;而不是主串。 KMP示例 首先我们来…

16.算法之字符串匹配算法

前言 字符串匹配是我们在程序开发中经常遇见的功能&#xff0c;比如sql语句中的like,java中的indexof,都是用来判断一个字符串是否包含另外一个字符串的。那么&#xff0c;这些关键字&#xff0c;方法&#xff0c;底层算法是怎么实现的么&#xff1f;本节&#xff0c;我们来探…

字符串匹配算法(BM)

文章目录 1. BM&#xff08;Boyer-Moore&#xff09;算法1.1 坏字符规则1.2 好后缀规则1.3 两种规则如何选择 2. BM算法代码实现2.1 坏字符2.2 好后缀2.3 完整代码2.4 调试 3. 总结 1. BM&#xff08;Boyer-Moore&#xff09;算法 思想&#xff1a;有模式串中不存在的字符&…

六种字符串匹配算法详解(含代码演示)

1. Brute-Force算法 2. Rabin-Karp Hash算法 3. Kmp算法 4. Kmp的优化算法 5. Sunday算法 6. Shift-And算法 ps&#xff1a;字符串匹配其实是单模匹配问题 1.Brute-Force 朴素匹配算法&#xff08;暴力匹配&#xff09; 时间复杂度&#xff1a;O(n*m) //返回 文本串s中第一…

字符串匹配算法(BF、KMP)

BF算法 描述&#xff1a; BF&#xff0c;Brute Force&#xff0c;暴力匹配的意思&#xff0c;是最简单直观的字符串匹配算法。假设有主串s1和子串s2&#xff0c;根据BF算法判断s1是否包含s2的步骤如下&#xff1a; 初始下标指针 i, j 分别指向s1, s2的首位置&#xff0c;若s1…

这可能是全网最好的字符串匹配算法讲解

点击上方 好好学java &#xff0c;选择 星标 公众号重磅资讯&#xff0c;干货&#xff0c;第一时间送达 今日推荐&#xff1a;14 个 github 项目&#xff01;个人原创100W 访问量博客&#xff1a;点击前往&#xff0c;查看更多为保证代码严谨性&#xff0c;文中所有代码均在 le…

Spring boot 项目(五)——AOP切面

一、AOP简介 1、在软件业&#xff0c;AOP为Aspect Oriented Programming的缩写&#xff0c;意为&#xff1a;面向切面编程&#xff0c;通过预编译方式和运行期间动态代理实现程序功能的统一维护的一种技术。 2、AOP是OOP的延续&#xff0c;是软件开发中的一个热点&#xff0c;也…

Spring AOP 切面(Aspect)应用详解

1. AOP 切面应用 下面是一个AOP切面的一个简单的应用实例 引入AOP依赖 <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-aop</artifactId></dependency>创建切面类对象 Aspect Component pub…

springboot实现AOP切面编程

概述 AOP(Aspect Oriented Programming) 即面向切面编程。面向切面是面向对象中的一种方式而已。在代码执行过程中&#xff0c;动态嵌入其他代码&#xff0c;叫做面向切面编程&#xff08;将交叉业务逻辑封装成成切面&#xff0c;利用AOP功能将切面织入到主业务逻辑———与主…

spring AOP切面及日志记录实现

目录 1.什么是AOP切面 2.理解AOP 3.AOP实例 1.自定义注解 2.创建一个切面类 3.将自定义注解标注在测试接口上 1.什么是AOP切面 AOP&#xff08;Aspect Oriented Programming&#xff09;&#xff0c;面向切面思想&#xff0c;是Spring的三大核心思想之一。 在项目中经常…

Aop切面自定义注解的使用

一&#xff1a;功能简介 本文主要记录如何使用aop切面的方式来实现日志记录功能。 主要记录的信息有: 操作人&#xff0c;方法名&#xff0c;参数&#xff0c;运行时间&#xff0c;操作类型(增删改查)&#xff0c;详细描述&#xff0c;返回值。 二&#xff1a;项目结构图 三…

AOP切面注解

一.前言 在以前的项目中&#xff0c;很少去关注spring aop的具体实现与理论&#xff0c;只是简单了解了一下什么是aop具体怎么用&#xff0c;看到了一篇博文写得还不错&#xff0c;就转载来学习一下&#xff0c;博文地址&#xff1a;http://www.cnblogs.com/xrq730/p/4919025.h…

AOP切面执行顺序

文章目录 一. 概述二. 讲述1. 单切面中各通知方法的执行顺序2. 多切面中各通知方法的执行顺序3. 多切面的通知方法中抛出异常 参考资料 一. 概述 本文主要讲述以下几点 单AOP切面时&#xff0c;各通知方法的执行顺序。多AOP切面时&#xff0c;多切面的执行顺序和各通知方法的执…

spring aop切面执行顺序

spring aop切面执行顺序 切面执行顺序 现有切面1、切面2对同一个切点按先后顺序执行&#xff08;切面1先于切面2执行&#xff09; 切面1&#xff1a;Before、After、AfterReturnning、AfterThrowing、Around 执行前、Around 正常执行、Around 异常执行、Around 执行后切面2&am…

一文带你搞定AOP切面

摘要&#xff1a;AOP在spring中又叫“面向切面编程”&#xff0c;是对传统我们面向对象编程的一个补充&#xff0c;主要操作对象就是“ 切面”&#xff0c; 可以简单的理解它是贯穿于方法之中&#xff0c;在方法执行前、执行时、执行后、返回值后、异常后要执行的操作。 本文分…

SpringBoot AOP切面实现

文章目录 一、AOP简介二、AOP体系与概念三、AOP实例1、创建SpringBoot工程2、添加依赖3、AOP相关注解3.1、Aspect3.2、Pointcut3.2.1、execution()3.2.2、annotation() 3.3、Around3.4、Before3.5、After3.6、AfterReturning3.7、AfterThrowing 一、AOP简介 AOP&#xff08;As…

AOP切面编程的理解

一、什么是Spring的AOP&#xff1f; AOP在spring中又叫“面向切面编程”&#xff0c;它可以说是对传统我们面向对象编程的一个补充&#xff0c;从字面上顾名思义就可以知道&#xff0c;它的主要操作对象就是“切面”&#xff0c;所以我们就可以简单的理解它是贯穿于方法之中&a…