推荐阅读:(程序员小灰)
总述
(m为主串的长度,n为子串的长度)
- 暴力的BF Brute Force暴力算法,直接单个比较,时间复杂度为O(m*n),java底层API中的indexOf就是BF算法直接实现的
- RK算法(Rabin-Karp两位发明者) 利用hash进行比较后再字符串比较。时间复杂度:O(n)
- BM算法,采用 “坏字符”+“好后缀”进行匹配,每次进行跳跃匹配,以此避免BF中单个字符移动进行无效匹配的次数。从右往左找“坏字符”;在前缀中找好后缀
- KMP算法,详见:KMP算法 字符串匹配
RK算法
简单介绍一下RK算法:
hash的方式有很多,我们要在“出现hash冲突”以及“计算hash的复杂度”之间权衡以后进行合适的选择。
这里我们采用a为1,b为2。。的**“按位相加”**的hash方式。
还有一种常见的hash方式:
转换成26进制数
既然字符串只包含26个小写字母,那么我们可以把每一个字符串当成一个26进制数来计算。
这样做的好处是大幅减少了hash冲突,缺点是计算量较大,而且有可能出现超出整型范围的情况,需要对计算结果进行取模。
bce = 2*(26^2) + 3*26 + 5 = 1435
我们采用“按位相加”的方式进行介绍
- 先将目标子串(模式串pattern)的hash计算出来:targethashCode
- 将主串中和pattern长度相等的子串的hash分别计算出来,注意,当有了第一个子串的hashCode1之后,下一个子串的hashCode2就=hashCode1-hash(左侧字符)+hash(右侧字符)。
- 当hashCode和targethashCode相等的时候,进行equals的比较即可
所以 每次子串和模式串之间的对比都是O1的复杂度,计算hash就是On时间复杂度,整体就是On
注意点:其实很容易产生冲突,如果冲突过多,就相当于每次都要equals,时间复杂度就退化到了BF算法的复杂度:O(m*n)。
BM算法
介绍查看:BM算法漫画介绍
BM算法的要点:“坏字符”+“好后缀”。
坏字符含义:
- 如果出现了“坏字符”,就找未匹配的模式串左侧最靠右的首个和“坏字符”相等的字符,然后挪过去
- 如果只是依赖“坏字符”原则,其实已经可以减少很多无效匹配了。但如果出现下图所示的情况,每次还是只移动一位,所以我们接下来可以再进行一个“好后缀”的原则匹配。
“好后缀”概念:主串的子串和模式串中都有共同的后缀“GCG”,这个就是好后缀。
如果此时发现模式串的其他位置anotherPlace还有好后缀,那么就可以挪动模式串,直接将anotherPlace和主串的好后缀对齐,就可减少比较次数
假如模式串中不存在anotherPlace,那么就在模式串的前缀中找是否有和“好后缀的后缀”相等的字符串,然后进行移动(这个就比较像KMP算法了)
现在我们已经知道什么是“坏字符”,什么是”好前缀“。那么我们怎么判断什么时候用坏字符规则,什么时候用好前缀规则呢?
答案很简单,直接每一轮分别按照两种规则计算相应的挪动距离,哪个距离长,就挪动相应的长度。
附上RK算法的代码和BM算法的简单版本
// RK算法
public static int rabinKarp(String str,String pattern) {
//主串长度
int m = str.length();
//模式串的长度
int n = pattern.length();
//计算模式串的hash值
int patternCode = hash(pattern);
//计算主串当中第一个和模式串等长的子串hash值
int strCode = hash(str.substring(0, n));
//用模式串的hash值和主串的局部hash值比较。
//如果匹配,则进行精确比较;如果不匹配,计算主串中相邻子串的hash值。
for(int i = 0; i < m - n + 1; i++) {
if (strCode == patternCode && compareString(i, str, pattern)) {
return i;
}
//如果不是最后一轮,更新主串从i到i+n的hash值
if (i < m - n) {
strCode = nextHash(str, strCode, i, n);
}
}
return -1;
}
private static int hash(String str) {
int hashcode =0;
//这里采用最简单的hashcode计算方式:
//把a当做1,把b当中2,把c当中3.....然后按位相加
for(int i =0; i < str.length(); i++) {
hashcode += str.charAt(i) - 'a';
}
return hashcode;
}
private static int nextHash(String str,int hash,int index,int n) {
hash -= str.charAt(index) - 'a';
hash += str.charAt(index + n) - 'a';
return hash;
}
private static boolean compareString(int i,String str,String pattern) {
String strSub = str.substring(i, i + pattern.length());
return strSub.equals(pattern);
}
public static void main(String[] args) {
String str = "aacdesadsdfer";
String pattern = "adsd";
System.out.println("第一次出现的位置:" + rabinKarp(str, pattern));
}
//在模式串中,查找index下标之前的字符是否和坏字符匹配
private static int findCharacter(String pattern, char badCharacter, int index) {
for (int i = index - 1; i >= 0; i--) {
if (pattern.charAt(i) == badCharacter) {
return i;
}
}
//模式串不存在该字符,返回-1
return -1;
}
public static int boyerMoore(String str, String pattern) {
int strLength = str.length();
int patternLength = pattern.length();
//模式串的起始位置
int start = 0;
while (start <= strLength - patternLength) {
int i;
//从后向前,逐个字符比较
for (i = patternLength - 1; i >= 0; i--) {
if (str.charAt(start + i) != pattern.charAt(i))
//发现坏字符,跳出比较,i记录了坏字符的位置
break;
}
if (i < 0) {
//匹配成功,返回第一次匹配的下标位置
return start;
}
//寻找坏字符在模式串中的对应
int charIndex = findCharacter(pattern, str.charAt(start + i), i);
//计算坏字符产生的位移
int bcOffset = charIndex >= 0 ? i - charIndex : i + 1;
start += bcOffset;
}
return -1;
}
public static void main(String[] args) {
String str = "GTTATAGCTGGTAGCGGCGAA";
String pattern = "GTAGCGGCG";
int index = boyerMoore(str, pattern);
System.out.println("首次出现位置:" + index);
}