最近整理了一下BM算法实现,浏览了很多帖子,发现一些帖子说的很多,但是给出的场景相对局限,要么就是model串是存在前后缀一致的,要么就是查找的结果都在最后。
本文考虑到以上两种情况:
1.好后缀可以匹配的字符在并不是前缀,是前缀中的一部分
2.如果主串已经匹配到model串了,但没匹配完,主串还有,那后面如何操作?
然后给出具体实现代码。
BM算法逻辑
用图简单说一下BM查找逻辑:
代码实现
/**
* BM算法
* @author SuMalago
*
*/
public class BMCalculator {
public static void main(String[] args) {
String partterns = "ABDCABDA";
String text = "ABDCADAAABDCABDADDABDCABDA";
BMSearch(text.toCharArray(), partterns.toCharArray());
}
/**
* BM查找
* @param mainStr 长串
* @param modelStr 短串
*/
private static void BMSearch(char[] mainStr, char[] modelStr){
//modelStr在mainStr的偏移位置
int start = 0;
int goodIndex = 0;
int badIndex = 0;
int[] prefixArray = initGoodSuffix(modelStr);
//-----以下是处理匹配完成后的后续操作,最后看这里,先看其他的----------start
int commonEleMaxIndex = modelStr.length;//默认取该串长度,即这个串里面所有字符没有重复的
for(int i=prefixArray.length-1;i>0;i--){
if(prefixArray[i] > -1){
//prefixArray数组的下标是后缀长度,值是最后一次相同后缀字符所在位置、
//下标越小,例如1,其实际在modelStr中越靠后,即元素位置=modelStr.length-1
commonEleMaxIndex = modelStr.length-i-prefixArray[i];
}
}
//----------------------end---------------------------------
while(modelStr.length + start <= mainStr.length){//确保不会超
//取出好后缀的最大偏移量
goodIndex = goodSuffix(mainStr,modelStr,start,prefixArray);
if(goodIndex == 0){
System.out.println(" BM is find : the index -> "+start);
//找到之后,但mainStr没循环完,直接查找最大后缀是匹配元素,然后使用好后缀的匹配规则后移
//找到此modelStr存在的最大的后缀,然后后移,比start+1要省时间
goodIndex = commonEleMaxIndex;
}
//取出坏字符的最大偏移量
badIndex = badChar(mainStr,modelStr,start);
//好后缀和坏字符,哪个大用哪个
start = start + (goodIndex >= badIndex ? goodIndex : badIndex);
}
}
/**
* 给modelStr使用的全量数组<br/>
* 这里是先把modelStr字符串中所有字符存在一个数组里
* 后面重复的位置的字符会替代之前的
* 因为:在查找时是倒着查找,所以会先取后面的字符位置
*
* @param modelStr 假设这个字符串都是由大写字母组成 ,自己实现也可以用ASCII码
* @return
*/
private static int[] initModelChr(char[] modelStr){
//一共26个大写字母,初始值都设为-1
//假设长字符串text中的某个字符在匹配modelStr时,该位置下的字符在modelStr中没找到,即返回-1
int[] existModelArray = new int[26];
for (int i = 0 ; i < 26 ; i ++) {
existModelArray[i] = -1;
}
//循环modelStr内容,将其内容里的大写字母对应到existModelArray中,后面重复的覆盖前面的
for (int i = 0 ; i < modelStr.length ; i++) {
existModelArray[modelStr[i] - 'A'] = i;//假设modelStr中第一个字符是B,则对应的位置就是 existModelArray[1]
}
return existModelArray;
}
/**
* 坏字符匹配模式下,需要移动的最大位置
* @param mainStr 主串
* @param modelStr 模式串
* @param start 模式串在主串中的起始位置
* @return 模式串可滑动距离,如果为0则匹配上
*/
private static int badChar(char[] mainStr, char[] modelStr, int start) {
int[] existModelArray = initModelChr(modelStr);
//主串的位置下标
int mainIndex = 0;
//主串中有的,但是modelStr中没有匹配到的 【坏字符】
char badChar = '\0';
//需要返回的坏字符的移动位置 默认0:没有坏字符,全匹配了
int returnBadCharMoveIndex = 0;
//倒着匹配
for(int i=modelStr.length-1; i >= 0 ;i--){
//主串实际下标的位置 = modelStr的起始位置+model倒序匹配的位置
mainIndex = start + i;
//检查没匹配上,mainStr[mainIndex]就是坏字符
if(mainStr[mainIndex] != modelStr[i]){
badChar = mainStr[mainIndex];
//检查modelStr中是否有这个字符,从而找出坏字符的匹配位置
if(existModelArray[badChar - 'A'] == -1){//没有
returnBadCharMoveIndex = i;
}else if(existModelArray[badChar - 'A'] > -1){//modelStr存在这个字符
returnBadCharMoveIndex = i - existModelArray[badChar - 'A'];
}
//检查到坏字符就跳出去
break;
}
}
return returnBadCharMoveIndex;
}
//好前缀的逻辑-------------------------------------
/**
* 寻找modelStr字符串中的 后缀的前面的所有能匹配的组合
* 比如ABCAB,后缀是B时查找相同元素B,即下标是1;后缀是AB时查找AB,下标是0
* @param modelStr
* @return int[] 下标是后缀长度,其值是与后缀长度一致的字符在modelStr最靠后的位置
*/
private static int[] initGoodSuffix(char[] modelStr) {
StringBuffer prefix = new StringBuffer();
StringBuffer suffix = new StringBuffer();
//随着每次的循环,后缀一直在递增 A.BA.DBA.CDBA.... 但不会过半,因为过半前面的就无法匹配后缀了
int tempSuffixCharLen = 0;
int[] prefixArray = new int[modelStr.length/2+1];
//这个数组下标是从1开始,0默认赋值-1
prefixArray[0] = -1;
int tempPrefixValue = -1;//默认没有 -1
for(int i=0;i<modelStr.length/2;i++){
//倒序 结果值往前插入 此时得到最新后缀,但此后缀之前只要存在的字符串和本后缀内容一致,就可以做映射(这里肯定包含前缀,但不仅限于前缀)
suffix.insert(0,modelStr[modelStr.length - 1 -i]);
tempSuffixCharLen = suffix.length();
/*
* 过滤本后缀之前的所有相同内容的字符串,要求:
* ①过滤的字符串长度要和suffix的长度一致,比如都是AB
* ②如果过滤过程中存在重复,则用后面的覆盖掉前面的,这是因为后面移动的时候如果移动前面的,会导致部分匹配内容漏掉
*/
for(int j=0;j<modelStr.length - 1 -i ;j++){
//每次进循环清空
prefix = new StringBuffer();
//这里面的循环是用来将后缀 前面的字符拼起来,使其长度和后缀suffix长度一致,因为你不知道后缀长度有多少,所以就得用循环
for(int x=j;x<modelStr.length - 1 -i ;x++){
prefix.append(modelStr[x]);
if(prefix.length() == tempSuffixCharLen){
//长度一致,内容一致
if((prefix.toString().equals(suffix.toString()))){
//能匹配上,代表第j次循环取tempSuffixCharLen个字符和后缀能匹配上
tempPrefixValue = j;
}
//注意这里,一旦长度相同就跳出去,后面的不循环匹配了,因为外循环有j++循环
break;
}
}
}
//本次tempSuffixCharLen长度的后缀匹配结束,下次循环tempSuffixCharLen+1
prefixArray[tempSuffixCharLen] = tempPrefixValue;
}
return prefixArray;
}
/**
*
* @param mainStr 主串
* @param modelStr 模式串
* @param start 模式串在主串中的起始位置
* @return 模式串可滑动距离,如果为0则匹配上
*/
private static int goodSuffix(char[] mainStr, char[] modelStr, int start,int[] prefixArray){
//第一步是这个,但是外面调用了,传进来里面不用调了
// int[] prefixArray = initGoodSuffix(modelStr);
//主串的位置下标
int mainIndex = 0;
//需要返回的坏字符的移动位置 默认0:没有坏字符,全匹配了
int returnGoodSuffixMoveIndex = 0;
//后缀内容
StringBuffer suffix = new StringBuffer();
//临时后缀长度,由长到短依次递减
int tempSuffixCharLen = 0;
mainFor:
//倒着匹配
for(int i=modelStr.length-1; i >= 0 ;i--){
//主串实际下标的位置 = modelStr的起始位置+model倒序匹配的位置
mainIndex = start + i;
//当匹配到坏字符时,坏字符后面的 都是好后缀
if(mainStr[mainIndex] != modelStr[i]){
//i位置上对应的已经是坏字符了,所以i后面才是 好后缀 即i+1
//如果最后一个就是坏字符,没有好后缀
if(i==modelStr.length-1){
returnGoodSuffixMoveIndex = -1;
break;
}else{
//好后缀是 从i+1 -> modelStr.length-1,由1个到多个的组合情况:例如字符串ABADB,其后缀 BADB ADB DB B
while(suffix.length() > 0){
//判断最长的后缀看能不能匹配到,然后依次后缀递减
tempSuffixCharLen = suffix.length();
if(prefixArray[tempSuffixCharLen] > -1){//存在一致的元素在前面
//要返回的长度=好后缀的下标-最后出现此后缀字符的下标
returnGoodSuffixMoveIndex = (i+1) - prefixArray[tempSuffixCharLen];
break mainFor;
}else{
//如果没有,则suffix去掉前面的一位,再循环检查
suffix = suffix.delete(0, 1);
}
}
}
}else{
//能匹配上,取后缀
suffix.insert(0,modelStr[i]);
}
}
return returnGoodSuffixMoveIndex;
}
}