字符串匹配算法——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算法解释。

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

"部分匹配"的实质是,有时候,字符串头部和尾部会有重复。比如,“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));

字符串匹配算法——JavaScript相关推荐

  1. 字符串匹配算法 -- BM(Boyer-Moore) 和 KMP(Knuth-Morris-Pratt)详细设计及实现

    文章目录 1. 算法背景 2. BM(Boyer-Moore)算法 2.1 坏字符规则(bad character rule) 2.2 好后缀规则(good suffix shift) 2.3 复杂度 ...

  2. Go 语言实现字符串匹配算法 -- BF(Brute Force) 和 RK(Rabin Karp)

    今天介绍两种基础的字符串匹配算法,当然核心还是熟悉一下Go的语法,巩固一下基础知识 BF(Brute Force) RK(Rabin Karp) 源字符串:src, 目标字符串:dest: 确认des ...

  3. 如何判断一个字符串在JavaScript中是否包含某个字符?

    本文翻译自:How to tell if a string contains a certain character in JavaScript? I have a page with a textb ...

  4. Boyer-Moore 字符串匹配算法

    字符串匹配问题的形式定义: 文本(Text)是一个长度为 n 的数组 T[1..n]: 模式(Pattern)是一个长度为 m 且 m≤n 的数组 P[1..m]: T 和 P 中的元素都属于有限的字 ...

  5. Java实现算法导论中朴素字符串匹配算法

    朴素字符串匹配算法沿着主串滑动子串来循环匹配,算法时间性能是O((n-m+1)m),n是主串长度,m是字串长度,结合算法导论中来理解,具体代码参考: package cn.ansj;public cl ...

  6. 字符串匹配算法Java_如何简单理解字符串匹配算法?

    这篇文章来说说如何简单理解KMP,BM算法.之前看过一些文章说,KMP算法很难理解. 可我并不觉得. 我反而觉得它容易理解.平时我们写java代码的时候, 判断一个字符串是否存在包含另一个字符串都是直 ...

  7. BF,KMP,BM三种字符串匹配算法性能比较

    三种最基本的字符串匹配算法是BF,KMP以及BM,BF算法是最简单直接的匹配算法,就是逐个比较,一旦匹配不上,就往后移动一位,继续比较,所以比较次数很都. 关于KMP和BM的详细介绍可以参考下面的两个 ...

  8. Python:对字符串匹配算法的分析

    问题描述 字符串匹配问题可以归纳为如下的问题: 在长度为n的文本T[1-n]中,查找一个长度为m的模式P[1-m].并且假设T,P中的元素都来自一个有限字母集合Ʃ.如果存在位移s,其中0≤s≤n-m, ...

  9. 字符串匹配算法(三):KMP(KnuthMorrisPratt)算法

    文章目录 KMP 原理 next数组的构建 代码实现 KMP 一提到字符串匹配算法,想必大家脑海中想到的第一个必然就是KMP算法,KMP算法的全称叫做KnuthMorrisPratt算法,与上一篇博客 ...

最新文章

  1. html5 jquery版工作流设计器,基于jQuery的web在线流程图设计器GooFlow
  2. python减法怎么表示_python运算符号之一的减法怎么用,你真的学会用python的使用方法了嘛...
  3. anaconda2/bin/../lib/libstdc++.so.6: version `GLIBCXX_3.4.20' not found Import No module named googl
  4. mac pandas文件路径_Mac进阶必看:如何利用Automator快速获取文件路径
  5. 解读:百度官方公告对于6.22、6.28事件解释
  6. 5、SQL Server数据库、T-SQL
  7. SpringBoot实战(八):集成Swagger
  8. 建立密钥,远程登录LINUX----ssh-keygen
  9. top命令显示内容的详细解释
  10. Java实例分析:宠物商店
  11. 计算机实战项目之 [含论文+任务书+中期检查表+答辩PPT+源码等]基于javaweb大学生助学贷款管理系统
  12. 利用MPU6050 + OLED屏显示3D矩形效果
  13. 烤薯条的全球与中国市场2022-2028年:技术、参与者、趋势、市场规模及占有率研究报告
  14. 一只青蛙跳向三个台阶_题目描述: k一只青蛙一次可以跳上1级台阶,也可以跳上2级。求该青蛙跳上一个n级的台阶总共有多少种跳法。...
  15. B站硬核up主稚晖君:对于有志学习嵌入式开发的软件工程师,我有这些建议!...
  16. SAP中汇率固定配置和应用分析测试
  17. java agent简介热部署SDK接入
  18. 马云不再是蚂蚁集团实控人
  19. 测试用例详解用例模板
  20. 新春特别策划:新春观影 与科幻电影难分舍的IT元素

热门文章

  1. 微信新用户暂停注册,预计恢复时间如下
  2. 怎样在iPhone或Mac上取消 Apple提供的付费订阅?
  3. linux查看日志(定位关键字)
  4. mysql的日常操作_MySQL日常操作
  5. 【CE】游戏内存修改 植物大战僵尸 太阳数量
  6. 谐云客户案例 | 华数传媒互联网电视业务的IT变革之路
  7. 点到反比例函数最短距离怎么求_数学原来靠“背”的,这几首顺口溜瞬间帮你记住数学重点公式和法则!...
  8. 如何比较两幅图的相似度
  9. python新建项目没有venv_pycharm配置venv虚拟环境
  10. MAC OS 如何修改“文件”或“文件夹”的“创建时间”和“修改时间”