字符串匹配方法对于在编辑文本程序时,能极大的提升响应效率,如网址查询搜索引擎,DNA序列匹配等。

基本定义

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

  • 文本(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 是有效位移。

字符串匹配算法通常分为两个步骤:预处理(Preprocessing)和匹配(Matching)。所以算法的总运行时间为预处理和匹配的时间的总和。下图描述了常见字符串匹配算法的预处理和匹配时间。

其中涉及到的解决字符串匹配的算法包括:朴素算法(Naive Algorithm)、Rabin-Karp 算法、有限自动机算法(Finite Automation)、 Knuth-Morris-Pratt 算法(即KMP Algorithm )、Boyer-Moore 算法、Simon 算法、Colussi 算法、Galil-Giancarlo 算法、Apostolico-Crochemore 算法、Horspool 算法和 Sunday 算法等。

朴素的字符串匹配算法

朴素的字符串匹配算法又称为暴力匹配算法(Brute Force Algorithm),它的主要特点是:

  1. 没有预处理阶段;
  2. 滑动窗口总是后移 1 位;
  3. 对模式中的字符的比较顺序不限定,可以从前到后,也可以从后到前;
  4. 匹配阶段需要 O((n - m + 1)m) 的时间复杂度;
  5. 需要 2n 次的字符比较;

很显然,朴素的字符串匹配算法 NAIVE-STRING-MATCHER 是最原始的算法,它通过使用循环来检查是否在范围 n-m+1 中存在满足条件 P[1..m] = T [s + 1..s + m] 的有效位移 s。

1 NAIVE-STRING-MATCHER(T, P)
2  n = length[T]
3  m = length[P]
4  for s = 0 to n - m
5     if P[1 .. m] = T[s + 1 .. s + m]
6          then print "Pattern occurs with shift" s

如上图中,对于模式 P = aab 和文本 T = acaabc,将模式 P 沿着 T 从左到右滑动,逐个比较字符以判断模式 P 在文本 T 中是否存在。

可以看出,NAIVE-STRING-MATCHER 没有对模式 P 进行预处理,所以预处理的时间为 0。而匹配的时间在最坏情况下为 Θ((n-m+1)m),如果 m = [n/2],则为 Θ(n^2)。

Rabin-Karp算法

对于一个时间复杂度为O((N-M+1)*M)的字符串匹配算法,即Rabin-Karp算法。Rabin-Karp算法的预处理时间是O(m), 匹配时间O((N-M+1)*M),既然与朴素算法的匹配时间一样,而且还多了一些预处理时间,那为什么我们 还要学习这个算法呢?

虽然Rain-Karp在最坏的情况下与朴素的世间复杂度一样,但是实际应用中往往比朴素算法快很多。而且该算法的 期望匹配时间是O(N+M)。在朴素算法中,我们需要挨个比较所有字符,才知道目标字符串中是否包含子串。那么, 是否有别的方法可以用来判断目标字符串是否包含子串呢?

答案是肯定的,确实存在一种更快的方法。为了避免挨个字符对目标字符串和子串进行比较, 我们可以尝试一次性判断两者是否相等。因此,我们需要一个好的哈希函数(hash function)。 通过哈希函数,我们可以算出子串的哈希值,然后将它和目标字符串中的子串的哈希值进行比较。 这个新方法在速度上比暴力法有显著提升。

Rabin-Karp算法的思想:

  1. 假设子串的长度为M,目标字符串的长度为N
  2. 计算子串的hash值
  3. 计算目标字符串中每个长度为M的子串的hash值(共需要计算N-M+1次)
  4. 比较hash值
  5. 如果hash值不同,字符串必然不匹配,如果hash值相同,还需要使用朴素算法再次判断

为了快速的计算出目标字符串中每一个子串的hash值,Rabin-Karp算法并不是对目标字符串的 每一个长度为M的子串都重新计算hash值,而是在前几个字串的基础之上, 计算下一个子串的 hash值,这就加快了hash之的计算速度,将朴素算法中的内循环的时间复杂度从O(M)将到了O(1)。

   public void search(char[] pat, char[] txt, int q) {int M = pat.length;int N = txt.length;int i, j;int d =256;  //模int p = 0;   // hash value for patint t = 0;   // hash value for txtint h = 1;for (i = 0; i < M-1; i++)h = (h*d)%q;for (i = 0; i < M; i++) {p = (d*p + pat[i])%q;t = (d*t + txt[i])%q;}for (i = 0; i <= N - M; i++) {if ( p == t ) {for (j = 0; j < M; j++) {if (txt[i+j] != pat[j])break;}if (j == M) {System.out.println("Pattern found at index: "+ i);}}if ( i < N-M ) {t = (d*(t - txt[i]*h) + txt[i+M])%q;if(t < 0)t = (t + q);}}}

Knuth-Morris-Pratt 算法(KMP)

Knuth-Morris-Pratt 字符串查找算法,简称为 KMP算法,常用于在一个文本串 S 内查找一个模式串 P 的出现位置。这个算法由 Donald Knuth、Vaughan Pratt、James H. Morris 三人于 1977 年联合发表,故取这 3 人的姓氏命名此算法。

KMP算法背后的基本思想是:每当我们检测到不匹配(在一些匹配之后),我们就已经知道了文本中的一些字符(因为它们在不匹配之前匹配了模式字符)。我们利用这些信息来避免匹配我们知道无论如何匹配的字符。

我们来观察一下朴素的字符串匹配算法的操作过程。如下图(a)中所描述,在模式 P = ababaca 和文本 T 的匹配过程中,模板的一个特定位移 s,q = 5 个字符已经匹配成功,但模式 P 的第 6 个字符不能与相应的文本字符匹配。

此时,q 个字符已经匹配成功的信息确定了相应的文本字符,而知道这 q 个文本字符,就使我们能够立即确定某些位移是非法的。例如上图(a)中,我们可以判断位移 s+1 是非法的,因为模式 P 的第一个字符 a 将与模式的第二个字符 b 匹配的文本字符进行匹配,显然是不匹配的。而图(b)中则显示了位移 s’ = s+2 处,使模式 P 的前三个字符和相应的三个文本字符对齐后必定会匹配。KMP 算法的基本思路就是设法利用这些已知信息,不要把 "搜索位置" 移回已经比较过的位置,而是继续把它向后面移,这样就提高了匹配效率。

前缀和后缀

"前缀"和"后缀"。 "前缀"指除了最后一个字符以外,一个字符串的全部头部组合;"后缀"指除了第一个字符以外,一个字符串的全部尾部组合。如下图所示,为字符串bread的前后缀:

部分匹配表

前缀函数:对于一个模式P,我们引入模式的前缀函数 π(Pi),π 包含有模式与其自身的位移进行匹配的信息。这些信息可用于避免在朴素的字符串匹配算法中,对无用位移进行测试。

 π[q] = max {k : k < q 且 Pk ⊐ Pq}  ,其中,k为已匹配字符数,Pk ⊐ Pq 表示P[1..k]为P[1..q]的后缀。

π[q],即"部分匹配值" 代表当前字符之前的字符串中,最长的共同前缀后缀的长度,也就是"前缀"和"后缀"的最长的共有元素的长度。下图给出了关于模式 P = ababababca 的完整前缀函数 π,可称为部分匹配表:

计算过程:

  • π[1] = 0,a 仅一个字符,前缀和后缀为空集,共有元素最大长度为 0;
  • π[2] = 0,ab 的前缀 a,后缀 b,不匹配,共有元素最大长度为 0;
  • π[3] = 1,aba,前缀 a ab,后缀 ba a,共有元素最大长度为 1;
  • π[4] = 2,abab,前缀 a ab aba,后缀 bab ab b,共有元素最大长度为 2;
  • π[5] = 3,ababa,前缀 a ab aba abab,后缀 baba aba ba a,共有元素最大长度为 3;
  • π[6] = 4,ababab,前缀 a ab aba abab ababa,后缀 babab abab bab ab b,共有元素最大长度为 4;
  • π[7] = 5,abababa,前缀 a ab aba abab ababa ababab,后缀 bababa ababa baba aba ba a,共有最大长度为 5;
  • π[8] = 6,abababab,前缀 .. ababab ..,后缀 .. ababab ..,共有元素最大长度为 6;
  • π[9] = 0,ababababc,前缀和后缀不匹配,共有元素最大长度为 0;
  • π[10] = 1,ababababca,前缀 .. ..,后缀 .. a ..,共有元素最大长度为 1;

所以P模式的部分匹配表,π 数组为:

其代码实现为KMP的辅助前缀函数prefix

  int[] prefix(char[] p) {int[] pi = new int[p.length];//π数组pi,1至m位,0位不取pi[1] = 0;//一个字符的前后缀都为空,公共元素长度为0int k = 0;//k为当前字串的前后缀组合中共有元素最长的长度//计算存储从2开始的子串的最大的公共长度π[i]值for (int i = 2; i <= pi.length; i++) {//如果已经存在公共元素长度k>0,且最大的公共长度的下一位和当前字符不同while (k > 0 && p[k+1] != p[i]) {/**若字符串为ababaca,当 i=5 时,子串为ababa,k=3,子串前后缀最长公共元素为aba*当 i=6 时,子串为ababac,因为abab不等于abac即 p[k+1]!=p[i];*所以最大公共元素长度k要重新计算,新的位置为前缀中的当前最长公共元素aba(1,2,3处的)和*其后缀中的当前最长公共元素aba(3,4,5处的)的最长公共元素a(1处和5处),即k=pi[k];*/        k = pi[k];}//最大的公共长度的下一位和当前字符相同if (p[k+1] == p[i]) k++; //则最大长度加一pi[i] = k;//记录i处子串的π值}return pi;}

测试函数为:

   public static void main(String[] args) {String T = "#BBC ABCDAB ABCDABCDABDE";String P = "#ABCDABD";char[] t = T.toCharArray();       char[] p = P.toCharArray();int n = t.length-1;int m = p.length-1;int[] pi = prefix(p);int q = 0;for (int i = 1;i <= n;i++) {while (q > 0 && p[q+1] != t[i]) q = pi[q];if (p[q+1] == t[i]) q++;if (q == m) {System.out.println("找到一个,T的位置:"+(i-m+1)+"到"+i);q = pi[q];//寻找下一个匹配的位置}}}static int[] prefix(char[] p) {int[] pi = new int[p.length];pi[1] = 0;//一个字符的前后缀都为空,公共元素长度为0int k = 0;//k为当前字串的前后缀组合中共有元素最长的长度//从2开始的子串的最大的公共长度π[i]值for (int i = 2; i < pi.length; i++) {//如果已经存在公共元素长度k>0,且最大的公共长度的下一位和当前字符不同while (k > 0 && p[k+1] != p[i])k = pi[k];//返回上一次的最大长度处//最大的公共长度的下一位和当前字符相同if (p[k+1] == p[i])k++; //则最大长度加一pi[i] = k;//记录i处子串的π值}return pi;}

推荐阮一峰老师的文章,讲的通俗易懂

Boyer-Moore算法

字符串匹配的Boyer-Moore算法

参考文章:

《算法导论》

算法导论(三)字符串匹配相关推荐

  1. 算法导论之字符串匹配

    当然对于前缀函数π一定可以起到有效位移的判断,导论还是给了证明.这里就说明其证明依据和结论. 前缀函数迭代引理:设P是长度为m的模式,其前缀函数为π,对q=1,2-,m,有π*[q]= {k:k< ...

  2. 【数据结构与算法】【字符串匹配】Trie树

    单模式串匹配 BF 算法和 RK 算法 BM 算法和 KMP 算法 多模式串匹配算法 Trie 树和 AC 自动机 一. 什么是"Trie树"? 1. 他是一种树形结构,是一种专门 ...

  3. 算法之路,带你轻松学废算法系列之字符串匹配(中)

    文章目录 字符串匹配中 前言 KMP算法 KMP算法的基本原理 失效函数计算方法 问题解答 KMP 算法复杂度分析 小结 Trie树 什么是Trie树 如何实现一棵 Trie 树? Trie树的构造 ...

  4. 算法之路,带你轻松学废算法系列之字符串匹配(下)

    文章目录 字符串匹配下 前言 引入 多模式串匹配算法:AC 自动机 AC自动机过滤敏感词 时间复杂度分析 小结 结尾 字符串匹配下 前言 大家好,我是魏果果哦,算法呢,是我们程序员一生无法避免的垫脚石 ...

  5. KMP算法(求解字符串匹配)

    提示:可搭配B站比特大博哥视频学习:传送门 (点击) 目录 前言 图解 代码 前言 KMP算法是一种改进的字符串匹配算法,由D.E.Knuth,J.H.Morris和V.R.Pratt提出的,因此人们 ...

  6. 字符串匹配rk算法c语言,字符串匹配问题(BFRK算法)

    1. 题目 有一个主串S={a,b,c,a,c,a,b,d,c},模式串T={a,b,d},请找出模式串在主串中第一次出现的位置 提示:不需要考虑字符串大小写问题,字符均为小写字母 2. BF算法 B ...

  7. 算法五——字符串匹配(上)

    文章内容.图片均来自极客时间. 如何借助哈希算法实现高效字符串匹配 1 概念和用途 字符串匹配:查找一个字符串A在字符串B中是否出现,这个过程就是字符串匹配.A称为模式串,B称为主串.主串的长度记为n ...

  8. 字符串匹配——KMP算法

    字符串匹配--KMP算法 ​ 字符串匹配是计算机编程中最常使用到的基础算法之一.字符串匹配相关的算法很多,Knuth-Morris-Pratt(KMP)算法是最常用的之一.最近在学习KMP算法,学习了 ...

  9. Java最佳实践–字符串性能和精确字符串匹配

    在使用Java编程语言时,我们将继续讨论与建议的实践有关的系列文章,我们将讨论String性能调优. 我们将专注于如何有效地处理字符串创建, 字符串更改和字符串匹配操作. 此外,我们将提供我们自己的用 ...

最新文章

  1. iOS限定UITextField的输入格式
  2. ExtJs xtype一览
  3. linux把目录下的文件设置属性为rx,LINUX的文件属性与目录配置
  4. library的英语怎么读音_【英语角】———学习方法分享
  5. JavaScript 使用对象及ES6中的class
  6. 使用C#创建一个简单的Windows服务
  7. 物联网核心安全系列——物联网安全需求
  8. ServletResponse的常用方法:getWriter,setContentType
  9. [Machine Learning]--PMI(Pointwise Mutual Information)
  10. 从乡巴佬到世界首富,连续7年蝉联全球最大公司:真正厉害的人,都敢于不走寻常路...
  11. 行为树 --- [4] 简单树
  12. 第8章 资源管理调度框架YARN
  13. 弦外雨,晚风急 吹皱芳华太无情
  14. 如何让机器产生意识之意识具象化
  15. 励志故事:拥有一颗热忱之心
  16. 使用电子签章确立在线医疗电子病历的法律地位
  17. 调用腾讯云使用图像验证码
  18. Linux系统编程——Daemon进程
  19. 《流浪地球》影评数据爬取分析
  20. 计算机pe病毒的感染过程,感染型PE病毒分析与专杀修复工具的开发.pdf

热门文章

  1. 软件测试硬件培训,软件测试和硬件测试的技巧
  2. Google Maps Api Geocoding 传递参数和返回参数的解析(Json)
  3. erp系统选型需满足哪些条件?一位erp选型专家分享经验
  4. Pascal VOC数据集 下载 百度云
  5. 前端模块化——彻底搞懂AMD、CMD、ESM和CommonJS
  6. 计算机课程设计-ssm在线点餐系统(沙箱支付)-javaweb外卖系统
  7. spice 0.14.0添加新功能
  8. 微信支付分支付免押订单租赁订单thinkphp5
  9. 数字IC后端实现问答(摘选自知识星球本周问答)
  10. 408计算机组成原理学习笔记——计算机系统概述