BM算法
构建坏字符规则
private static int[] generateBadChar(String s){
int[] barChar = new int[256];
Arrays.fill(barChar, -1);
for (int i = 0; i < s.length(); i++){
barChar[s.charAt(i)] = i;
}
return barChar;
}
字符这里是采用字符的ASCII码记录字符在模式串中出现的最后位置,如果在遍历模式串的过程中,后面重复的字符会覆盖掉原来记录过的下标值。这样在barChar
的数组中记录的是所有字符出现的最后的下标位置。
这里的初始值-1
也是有意义的,-1
表示对应的字符没有在模式串出现过。这是利用坏字符规则,主串i
移动可以直接移动到出现坏字符的下一位。
比如说:
这里主串和模式串匹配时,出现坏字符S
和E
不匹配,而坏字符S
在模式串中没有出现,在barChar
数组中对应记录的值为-1
,这是i=0, j=6
的,此时主串就可以移动i = j - barChar[s.char(i+j)]
位,即6-(-1) = 7
。
构建好后缀规则
private static void generateGoodSuffix(String pattern, int[] suffix, boolean[] prefix){
Arrays.fill(suffix, -1);
Arrays.fill(prefix, false);
for (int i = 0; i < pattern.length() - 1; i++) {
int j = i, k = 0;
while (j >= 0 && pattern.charAt(j) == pattern.charAt(pattern.length() - 1 - k)) {
suffix[++k] = j--;
}
if (j == -1){
prefix[k] = true;
}
}
}
首先我们要对两个数组进行说明:
suffix
数组记录的是模式串中对应后缀长度子串的末尾索引,比如说abcabc
,它的suffix
数组中记录的值为[-1, 2, 1, 0, -1, -1]
,其中suffix[2] = 1
表示满足后缀长度为2
的后缀字串末尾索引是1
。即现在是末尾的bc
,长度为2。而在模式串后面又出现了bc
了,那bc
的末尾索引是1,即是b
的下标。注意一下,我说的后面是从右向左看这个模式串。
总结以下就是**suffix
数组的下标表示的是能和模式串前面能够匹配的长度,而记录的值是满足条件的后缀字串的末尾下标**。
prefix
数组,记录了模式串中每个位置的前缀字串是否是模式串的前缀。前缀子串是指一个字符串的开头部分。还是拿abcabc
举例,它的prefix
数组记录的值是[false, false, false, true, false, false]
,prefix[3] = true
表示表示模式串中长度为 3 的前缀子串是模式串的前缀。即abc
和abc
能够匹配。下标也可以理解为能够匹配的长度。只有当前缀子串的开头部分和结尾部分重合时,才会将对应的 prefix 数组元素设置为 true。
和suffix
数组不同的是,suffix
数组是匹配是可以在模式串中间出现重复的部分,而prefix
必须是要到下标为 0 的位置。
代码中要需要注意的是i < pattern.length() - 1
,i
不能等于pattern.length() - 1
。如果等于了相当于j
和pattern.length() - 1 - k
从同一个位置开始,自己和自己匹配,破坏了后缀子串的条件,会导致整个记录更新,错误记录。
根据好后缀规则确定滑动位数
private static int moveByGoodSuffix(int j, int[] suffix, boolean[] prefix) {
int k = suffix.length - 1 - j;
if (suffix[k] != -1){
return j - suffix[k] + 1;
}
for (int r = j + 2; r < suffix.length; r++) {
if (prefix[suffix.length - r]) {
return r;
}
}
return suffix.length;
}
这里的j
表示的是遍历到坏字符的位置,k
则是前面满足匹配的字符串长度。根据k
这个长度去查看模式串后面是否有满足长度的后缀字符串。如果suffix[k] != -1
表示有,则计算返回移动的步数,j
是坏字符的位置,suffix[k]
是能够匹配后缀字符串末尾索引,两者相减即两个索引之间相差长度,再加 1 就是最后需要移动的步数。
如上i = 2, j = 2
,在主串i+j
和模式串j
匹配错误了,利用好后缀的原则,此时的k = 5
,没有好后缀,利用prefix
数组找到最短的前缀,此时prefix[3] = true
,移动结果为 8 -
说明一下r
表示的是可移动的步数,表示模式串向右滑动r位。之所以从j+2
开始,是我们知道[j+1, length-1]
的好后缀已经不匹配了,因为j
是坏字符,从j+1
当length-1
的位置如果存在好的后缀在suffix
的条件中就会进行处理了。所以走prefix
条件一定是从j+2
开始。length-r
就是能够进行匹配的长度。
如果还没有后缀字符串就整体移动。
BM算法
public static int strBM(String text, String pattern){
// 坏字符规则
int[] badChar = generateBadChar(pattern);
// 好后缀规则
int[] suffix = new int[pattern.length()];
boolean[] prefix = new boolean[pattern.length()];
generateGoodSuffix(pattern, suffix, prefix);
int i = 0;
while (i <= text.length() - pattern.length()) {
int j = pattern.length() - 1;
while (j >= 0 && text.charAt(i + j) == pattern.charAt(j)) {
j--;
}
if (j < 0) return i;
int moveLen1 = j - badChar[text.charAt(i + j)];
int moveLen2 = 0;
if (j < pattern.length() - 1) {
moveLen2 = moveByGoodSuffix(j, suffix, prefix);
}
i += Math.max(moveLen1, moveLen2);
}
return -1;
}
首先构建好坏字符规则和好后缀规则。从模式串从后向前进行匹配,这里j
是模式串的指针,i+j
是主串上的指针。当j
比0小,匹配成功,此时i
指向的位置就是能够匹配的字符串的第一个字符下标,也因此得出i
的范围[0, text.length()-pattern,length()]
。
moveLen1
是根据坏字符规则得到的滑动距离,moveLen2
是根据好后缀得到的滑动距离,取二者的最大滑动距离,算法效率高。