算法导论(三)字符串匹配
字符串匹配方法对于在编辑文本程序时,能极大的提升响应效率,如网址查询搜索引擎,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 位;
- 对模式中的字符的比较顺序不限定,可以从前到后,也可以从后到前;
- 匹配阶段需要 O((n - m + 1)m) 的时间复杂度;
- 需要 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算法的思想:
- 假设子串的长度为M,目标字符串的长度为N
- 计算子串的hash值
- 计算目标字符串中每个长度为M的子串的hash值(共需要计算N-M+1次)
- 比较hash值
- 如果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 ..,后缀 .. 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算法
参考文章:
《算法导论》
算法导论(三)字符串匹配相关推荐
- 算法导论之字符串匹配
当然对于前缀函数π一定可以起到有效位移的判断,导论还是给了证明.这里就说明其证明依据和结论. 前缀函数迭代引理:设P是长度为m的模式,其前缀函数为π,对q=1,2-,m,有π*[q]= {k:k< ...
- 【数据结构与算法】【字符串匹配】Trie树
单模式串匹配 BF 算法和 RK 算法 BM 算法和 KMP 算法 多模式串匹配算法 Trie 树和 AC 自动机 一. 什么是"Trie树"? 1. 他是一种树形结构,是一种专门 ...
- 算法之路,带你轻松学废算法系列之字符串匹配(中)
文章目录 字符串匹配中 前言 KMP算法 KMP算法的基本原理 失效函数计算方法 问题解答 KMP 算法复杂度分析 小结 Trie树 什么是Trie树 如何实现一棵 Trie 树? Trie树的构造 ...
- 算法之路,带你轻松学废算法系列之字符串匹配(下)
文章目录 字符串匹配下 前言 引入 多模式串匹配算法:AC 自动机 AC自动机过滤敏感词 时间复杂度分析 小结 结尾 字符串匹配下 前言 大家好,我是魏果果哦,算法呢,是我们程序员一生无法避免的垫脚石 ...
- KMP算法(求解字符串匹配)
提示:可搭配B站比特大博哥视频学习:传送门 (点击) 目录 前言 图解 代码 前言 KMP算法是一种改进的字符串匹配算法,由D.E.Knuth,J.H.Morris和V.R.Pratt提出的,因此人们 ...
- 字符串匹配rk算法c语言,字符串匹配问题(BFRK算法)
1. 题目 有一个主串S={a,b,c,a,c,a,b,d,c},模式串T={a,b,d},请找出模式串在主串中第一次出现的位置 提示:不需要考虑字符串大小写问题,字符均为小写字母 2. BF算法 B ...
- 算法五——字符串匹配(上)
文章内容.图片均来自极客时间. 如何借助哈希算法实现高效字符串匹配 1 概念和用途 字符串匹配:查找一个字符串A在字符串B中是否出现,这个过程就是字符串匹配.A称为模式串,B称为主串.主串的长度记为n ...
- 字符串匹配——KMP算法
字符串匹配--KMP算法 字符串匹配是计算机编程中最常使用到的基础算法之一.字符串匹配相关的算法很多,Knuth-Morris-Pratt(KMP)算法是最常用的之一.最近在学习KMP算法,学习了 ...
- Java最佳实践–字符串性能和精确字符串匹配
在使用Java编程语言时,我们将继续讨论与建议的实践有关的系列文章,我们将讨论String性能调优. 我们将专注于如何有效地处理字符串创建, 字符串更改和字符串匹配操作. 此外,我们将提供我们自己的用 ...
最新文章
- 特定场景下SQL的优化
- Android UI线程和非UI线程
- 一文读懂如何用LSA、PSLA、LDA和lda2vec进行主题建模
- C++Builder编程中动态更改自定义打印纸张
- node.weiChat
- NLP --- 隐马尔可夫HMM(第一个、第二个问题解决方案)
- 面试题java的权限_Java shiro面试题
- 这些藏在成都的 NB 互联网公司
- Oracle数据库基础练习(一):Oracle数据库查询操作练习81题
- 柬埔寨之旅——穿越千年,感受震撼
- snmp工具_运维超级好用工具大PK,你在用哪个?
- 《爱してる 》歌词与音译(大爱哇)
- 分享新手电商(淘宝、拼多多、楚楚街)上货经验
- 论信息系统人力资源管理
- 【零基础学Python】爬虫篇 :第十四节--爬虫+词云解决实际问题
- 自制 计算机主板,DIY从入门到放弃:主板避坑简单三步走
- Java测试字节流和字符流,以及带缓存的字符流读取速度对比
- 阿里巴巴人工智能实验室(Ali A.I. Labs)负责人浅雪近期问答整理
- matlab:圆内均匀随机取点
- java编写股票交易软件有哪些,java开发程序源代码_炒股软件说明-小S股票