本文是数据结构与算法之美的学习笔记
字符串的匹配我们在平时工作中经常用到,每个语言中都有其对应的字符串的匹配方法,比如Java中的indexOf(),Python中的find()等他们底层都依赖了各种字符串的匹配算法。
字符串的匹配算法有:单模式串匹配算法(BF算法,RK算法,BM算法,KMP算法),
多模式串匹配算法(Trie树,AC自动机)
BF(Brute Force)算法
基础概念:如果我们在A字符串中查找B字符串,那么A就是主串,B就是模式串主串的长度设为n,模式串的长度设为m。
BF算法就是拿模式串m,从主串n的第0位开始匹配,如果匹配不成功,则后移一位继续匹配
第一步
主串 :baddef (匹配不上后移一位)
模式串:abc
第二步
主串 :baddef (匹配不上后移一位)
模式串:abc
第三步
主串 :baddef (匹配不上后移一位)
模式串:abc
第四步
主串 :baddef (匹配不上结束)
模式串:abc
缺点:效率不高,如果主串是aaaaaaaaaa…很多a,模式串是aaaab,那么需要对比的次数非常多
优点:简单不容易出错,在主串和模式串都不太长的情况下这种方法很实用
RK(Rabin-Karp)算法
在上面的BF算法中,如果主串是n,模式串是m,一位一位的往后移动,最坏的情况下将会有n-m+1个子串需要匹配才能找到跟模式串匹配的字符串,比较复杂。
RK算法:数字的匹配比字符串快速,就把主串中的这n-m+1的子串分别求哈希值,然后在分别跟模式串的哈希值进行比较。如果哈希值不一样那肯定不匹配,如果哈希值一样,因为哈希算法存在哈希冲突,这时候在拿模式串跟该子串对比一下就好了。
虽然模式串跟子串的对比速度提高了,但是我们事先需要遍历主串,逐个求子串的哈希值,这部分也挺耗时的,所以需要设计比较高效的哈希算法尽量的减少哈希冲突的产生。
BM(Boyer-Moore)算法
上面两种字符串匹配算法都有缺点,BF算法在极端情况下效率会很低,RK算法需要有一个很好的哈希算法,而设计一个好的哈希算法并不简单,有没有尽可能的高效,极端情况下效率退化也不大的算法呢,下面看看BM算法。
BM算法是一种非常高效的算法,各种记事本的查找功能一般都是采用的这种算法,有实验统计它的效率是KMP算法的3-4倍。
BF算法的核心思想是:在模式串和主串的匹配过程中,当遇到不匹配的字符的时候,BF和RK的做法是往后移动一位,然后从模式串的第一个字符开始从新匹配,而BM算法的思想是找到一种可以一下子移动好几位的规律,直接移动好几位,跳过那些肯定不能匹配的字符,这样匹配的次数就少很多。比如下面:
主串:abcacabdc
模式串:abd
第一次匹配到c,c在模式串中是不存在的,所以我们可以直接移动到c的下一位继续匹配。
BM算法包括两个规则,“坏字符规则”和“好后缀规则”
BM算法是从模式串的尾部开始比较的
(1)坏字符规则:
主串: abcacabdc
模式串:abd
从模式串的尾部开始匹配,如果匹配不成功,这个字符串就叫做“坏字符”,这里的坏字符就是c
然后查找模式串中是否有这个坏字符c,这里是没有,所以c之前的子串肯定不能跟模式串匹配成功,所以我们可以直接移动到c的后面一位继续匹配
主串:abcacabdc
模式串:abd
移动完之后在比较,这时候d跟a还是不匹配,a就是坏字符,这时候发现a是在模式串中存在的,所以我们就把模式串移动到模式串中的a跟这个坏字符a对齐的位置。
注意:如果模式串中有多个a这里选择模式串中最靠后的那个a,防止本来能匹配的被略过,滑动完如下
主串:abcacabdc
模式串:abd
上面就是使用坏字符规则来匹配,但是这也会有极端情况,比如主串aaaaaaaaaaaaaaaa,模式串baaa,这时候使用坏字符规则,会发现倒着移动了,这时候就需要用到好后缀规则了。
(2)好后缀规则:
好后缀和坏字符的思想一样
主串:abcacabcbcbacabc
模式串:cbacabc
上面的匹配从后往前,bc已经匹配成功,到了c和a匹配不成功了,这时候bc叫做好后缀,c叫做坏字符,那使用坏字符还是使用好后缀移动呢?我们可以计算好后缀和坏字符往后滑动的位数,取两个数中最大的往后滑动。
注意: 好后缀的滑动并不是直接滑动到bc的后面,这样会出现问题的,比如上面的例子,其实c是可以和后面的字符合成模式串的,直接移动到c的后面就错过了匹配。正确应该移动到c的下面如下
主串:abcacabcbcbacabc
主串:abcacabcbcbacabc
模式串:cbacbac
所以说,在好后缀移动的时候,我们需要在检查一下已经匹配好的好后缀的后缀(比如abc的后缀就是c,bc),是否跟模式串的前缀子(abc的前缀子串就是a,ab)串匹配。我们从中找出一个最长的匹配,把模式串移动到跟这个匹配到的串对齐的位置就好了。
BM算法完整代码:
// a,b 表示主串和模式串;n,m 表示主串和模式串的长度。
public int bm(char[] a, int n, char[] b, int m) {
int[] bc = new int[SIZE]; // 记录模式串中每个字符最后出现的位置
generateBC(b, m, bc); // 构建坏字符哈希表
int[] suffix = new int[m];
boolean[] prefix = new boolean[m];
generateGS(b, m, suffix, prefix);
int i = 0; // j 表示主串与模式串匹配的第一个字符
while (i <= n - m) {
int j;
for (j = m - 1; j >= 0; --j) { // 模式串从后往前匹配
if (a[i+j] != b[j]) break; // 坏字符对应模式串中的下标是 j
}
if (j < 0) {
return i; // 匹配成功,返回主串与模式串第一个匹配的字符的位置
}
int x = j - bc[(int)a[i+j]];
int y = 0;
if (j < m-1) { // 如果有好后缀的话
y = moveByGS(j, m, suffix, prefix);
}
i = i + Math.max(x, y);
}
return -1;
}
// j 表示坏字符对应的模式串中的字符下标 ; m 表示模式串长度
private int moveByGS(int j, int m, int[] suffix, boolean[] prefix) {
int k = m - 1 - j; // 好后缀长度
if (suffix[k] != -1) return j - suffix[k] +1;
for (int r = j+2; r <= m-1; ++r) {
if (prefix[m-r] == true) {
return r;
}
}
return m;
}
KMP(Knuth Morris Pratt)算法
模式串跟主串左端对齐,先比较第一个字符,如果不一样就,模式串后移,直到第一个字符相等
第一个字符匹配上之后在匹配第二个,直到有不相等的为止,比如下面
主串:cdababaeabac
模式串:ababacd
e和c不匹配,e就可以理解为坏字符,ababa可以理解为好前缀,那移动几位呢?
我们拿好前缀本身,在它的后缀子串中查找最长的那个可以跟好前缀的前缀子串匹配的。
移动位数 = 已匹配的字符数 - 对应的部分匹配值的长度
如何求这个对应的匹配值呢?,这个不涉及到主串只需要根据模式串就可以求出来。
比如这里的模式串是 ababacd
代码实现
// a, b 分别是主串和模式串;n, m 分别是主串和模式串的长度。
public static int kmp(char[] a, int n, char[] b, int m) {
int[] next = getNexts(b, m);
int j = 0;
for (int i = 0; i < n; ++i) {
while (j > 0 && a[i] != b[j]) { // 一直找到 a[i] 和 b[j]
j = next[j - 1] + 1;
}
if (a[i] == b[j]) {
++j;
}
if (j == m) { // 找到匹配模式串的了
return i - m + 1;
}
}
return -1;
}
// b 表示模式串,m 表示模式串的长度
private static int[] getNexts(char[] b, int m) {
int[] next = new int[m];
next[0] = -1;
int k = -1;
for (int i = 1; i < m; ++i) {
while (k != -1 && b[k + 1] != b[i]) {
k = next[k];
}
if (b[k + 1] == b[i]) {
++k;
}
next[i] = k;
}
return next;
}