String-Matching-Algorithm
字符串匹配
BF算法
算法概念
BF算法(Brute-Force算法)又称暴力检索算法,是最好想到也最容易实现的算法。
算法分析
BF算法从主串S的第pos个字符开始,和模式串T的第一个字符进行比较,若相等,则继续比较后续字符;否则回溯到主串S的第pos+1个字符开始重新和模式串T进行比较。以此类推,直至模式串T中的每一个字符依次和主串S中的一个连续的字符序列相等,则称模式匹配成功,此时返回模式串T的第一个字符在主串S中的位置;否则主串中没有和模式串相等的字符序列,称模式匹配不成功。
BF算法的思想比较简单,但当在最坏情况下,算法的时间复杂度为$O(MN)$,其中M和N分别是主串和模式串的长度。这个算法的主要事件耗费在失配后的比较位置有回溯,因而比较次数过多。为降低时间复杂度可采用无回溯的算法。
RK算法
之前在讨论BF算法的时候,我们说过关于模式串长度m,和主串长度n,那么在主串中就会有n-m+1个长度为m的子串,我们只需要暴力的一一对比n-m+1个子串与模式串,就可以找出主串中与模式串匹配的子串。但是这样就会出现一个问题,在每次检查子串和模式串是否匹配的时候,需要依次对比每个字符,比较耗时,所以我们现在就需要稍微进行改造,增加哈希算法,来加快子串与模式串的匹配,所以也就产生了RK算法。
算法概念
RK算法(Robin-Karp算法)又被称为哈希检索算法,是对BF算法的进一步优化,很巧妙的使用了哈希算法,让匹配的效率有了很大的提升。
算法分析
具体的思路是通过哈希算法对主串中的n-m+1个子串分别求哈希值,然后与模式串的哈希值进行比较。如果某个子串的哈希值等于模式串,那就说明子串与模式串相匹配。因为哈希值是一个数字,数字之间比较是否相等是非常快速的,所以模式串和子串的比对的效率就提高了。在找到具有相同hash值的子串后,我们实际实现时大多会再次使用BF算法扫描一遍确认是否相同。
RK算法分为子串hash值计算和比较子串hash值两部分。对于计算字串的哈希值,我们可以设计特殊的哈希算法,只需要扫描一遍主串就能得到所以子串的哈希值,因此,这一部分的时间复杂度为O(n)。由于算法实现具有再次扫描确认结果的环节,理论上最差时间复杂度与BF算法相同也为$O(MN)$,但实际hash冲突情况并不多,期望时间复杂度为$O(M+N)$。
KMP算法
算法概念
KMP 算法是 D.E.Knuth、J,H,Morris 和 V.R.Pratt 三位神人共同提出的,称之为 Knuth-Morria-Pratt 算法,简称 KMP 算法。该算法相对于 Brute-Force(暴力)算法有比较大的改进,主要是消除了主串指针的回溯,从而使算法效率有了某种程度的提高。
算法分析
KMP算法主要分为两个部分——获得next数组和模式匹配
获得next数组
获得next数组属于对需要匹配子串进行的预处理操作,主要是通过形成“部分匹配值”来避免进行模式匹配的回溯。而这个“部分匹配值”则是指字符串前缀和后缀共有元素的长度。
如对于"ABCDABD"这个串,我以"ABCDAB"举例,它含有前缀集合[A,AB,ABC,ABCD,ABCDA],后缀集合[BCDAB,CDAB,DAB,AB,B],共有元素AB,长度为2。
通过类似过程,对于"ABCDABD"这个串,可以获得下表
| 搜索词 | A | B | C | D | A | B | D |
| ---------- | ---- | ---- | ---- | ---- | ---- | ---- | ---- |
| 部分匹配值 | 0 | 0 | 0 | 0 | 1 | 2 | 0 |
而获得next数组的求解代码如下:
可以参考我的另一篇博客——datastruct_3,这篇博客有nextval数组的求解,算是对next数组的进一步优化。
1 | void get_next(String T, int next[]) { |
模式匹配
求出next数组后,KMP算法就很简单了,基本就是在套用公式进行移动子串即可。
移动位数=已匹配的位数-最后一个匹配字符的部分匹配值
通过套用这个公式在模式匹配时进行子串的移动,就可以避免回溯过程的发生(当然采用上文链接中实现的nextval数组还可以匹配的更快,在此不具体展开),具体KMP算法实现代码如下:
1 | int Index_KMP(String S, String T) { |
算法复杂度
由于KMP算法没有子串回溯的过程,时间复杂度是非常优秀的$O(N)$
BM算法
BM算法是KMP算法执行效率的3~5倍,各种记事本大多Ctrl+F都采用了这种算法。
算法概念
BM算法(Boyer-Moore字符串搜索算法)是一种非常高效的字符串搜索算法。它由Bob Boyer和J Strother Moore设计于1977年。此算法仅对搜索目标字符串(关键字)进行预处理,而非被搜索的字符串。
算法分析
虽然Boyer-Moore算法的执行时间同样线性依赖于被搜索字符串的大小,但是通常仅为其它算法的一小部分:它不需要对被搜索的字符串中的字符进行逐一比较,而会跳过其中某些部分。通常搜索关键字越长,算法速度越快。它的效率来自于这样的事实:对于每一次失败的匹配尝试,算法都能够使用这些信息来排除尽可能多的无法匹配的位置。
BM算法原理
BM算法定义了两个规则:
- 坏字符规则:当文本串中的某个字符跟模式串的某个字符不匹配时,我们称文本串中的这个失配字符为坏字符,此时模式串需要向右移动,移动的位数 = 坏字符在模式串中的位置 - 坏字符在模式串中最右出现的位置。此外,如果"坏字符"不包含在模式串之中,则最右出现位置为-1。
- 好后缀规则:当字符失配时,后移位数 = 好后缀在模式串中的位置 - 好后缀在模式串上一次出现的位置,且如果好后缀在模式串中没有再次出现,则为-1。
这样的原则理解起来比较复杂(感觉这两个规则更多是为编程服务的,对于解释原理作用不大),简而言之就是要注意两个原则即可——
- 原字符串和子串左端对齐,从尾部向前比较。如果发现全部匹配则匹配成功
- 在匹配失败时寻找子串是否出现原字符串中的字符,有则移动到响应位置,否则直接移动到当前整体子串的后一个位置。
完整代码实现
1 | public static int pattern(String pattern, String target) { |
算法复杂度
BM算法的时间复杂度最差情况可以达到$O(MN)$,但在实际使用时多表现为$O(N)$的时间复杂度。
Sunday算法
算法概念
Sunday算法由Daniel M.Sunday在1990年提出,它的思想跟BM算法很相似,只不过Sunday算法是从前往后匹配,在匹配失败时关注的是主串中参加匹配的最末位字符的下一位字符。
算法分析
Sunday算法原理
Sunday算法是从前往后匹配,在匹配失败时关注的是主串中参加匹配的最末位字符的下一位字符。
-
如果该字符没有在模式串中出现则直接跳过,即移动位数 = 模式串长度 + 1;
-
否则,其移动位数 = 模式串长度 - 该字符最右出现的位置(以0开始) = 模式串中该字符最右出现的位置到尾部的距离 + 1。
偏移表
在预处理中,计算大小为的偏移表。
$$
f(n)
\begin{cases}
m−max{i<m|P[i]=w}, &if\ w\ is\ in\ p[0…m-1]\
m + 1, &otherwise
\end{cases}
$$
完整代码实现
1 |
|
算法复杂度
Sunday算法的时间复杂度最差情况可以达到$O(MN)$,但在实际使用时多表现为$O(N)$的时间复杂度。
- Title: String-Matching-Algorithm
- Author: Charles
- Created at : 2023-08-11 17:12:51
- Updated at : 2023-08-15 20:59:22
- Link: https://charles2530.github.io/2023/08/11/string-matching-algorithm/
- License: This work is licensed under CC BY-NC-SA 4.0.