字符串匹配算法
匹配原理:
字符串A :a b c d e f h
字符串B : c d e f
通过观察可知,字符串B是字符串A的子串,且B在A中第一次出现的位置是2,所以直接返回2,若无法匹配到子串则返回-1,一般我们统一将这里的A称为主串,B称为模式串。
方法一:BF算法(Brute Force 暴力算法)
暴力匹配法,即从主串的首位开始,把主串和模式串的字符进行逐个比较,若第一位就不同,则将模式串进行后移一位,再次重新开始比较;若第一位相同,则进行主串与模式串的第二位,第三位的依次比较。
- 假设主串的长度是m,模式串的长度是n,BF算法的最坏时间复杂度为O(mn);
- 该种方法实现简单,但是执行效率很低;
方法二:RK算法(Rabin-Karp 哈希匹配算法)
哈希匹配法,即将原先主串和模式串的字符比较转换成对应哈希值比较,这样的要容易得多。具体的哈希转换方法有很多,例如,按位相加法、转换成26进制法。由于主串通常要长于模式串,把整个主串转化成hashcode是没有意义的,只有比较主串当中和模式串等长的子串才有意义,当哈希值相等时,再模仿BF算法,进行两个字符串的逐项比较。
hashcode = hash(string)
- 哈希法可以通过优化字符串累加方法,即新子串的hash值都用上一次子串进行简单的增量来计算;
- 通使用优化方法,最终RK算法的时间复杂度可以优化为O(n);
- 与BF算法相比,免去了很多无谓的字符比较,时间复杂度上有很大提高;
- RK算法的缺点在于哈希冲突,每一次哈希冲突时都要进行子串和模式串的逐个比较,如果冲突过多,RK算法就会退化成BF算法。
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));
方法三:BM算法(坏字符和好后缀算法)
坏字符规则
即是指模式串和子串当中不匹配的字符,当模式串和主串的第一个等长子串比较时,从右侧开始确定坏字符(检测顺序相反,是从字符串最右侧向最左侧检测),如果坏字符在模式串中不存在,则直接把模式串挪到主串坏字符的下一位。
- 坏字符的位置越靠右,下一轮模式串的挪动跨度就可能越长,节省的比较次数也就越多;
//在模式串中,查找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);
}
好后缀规则
好后缀就是指模式串和子串当中相匹配的后缀。即当子串和模式串不匹配时,但模式串和子串存在好后缀,且在模式串中可以找到与好后缀相同的片段,这样就可以直接移动模式串中的相同片段与模式串的好后缀对齐,从而实现快速位移;但是当不存在这样的相同片段时,切记不可一次性把模式串移动到好后缀的后面,要判断模式串的前缀是否与好后缀的后缀相匹配,以免移动过多而错过。
何时采用坏字符或者好后缀规则,并没有直接结论,需要分别计算下一轮模式串移动的长度并进行比较,可以使模式串移动更多的规则,就是更好的方法。
方法四:KMP算法(最长可匹配前后缀子串算法)
即在已匹配的前缀当中寻找到最长可匹配后缀子串和最长可匹配前缀子串,在下一轮直接把两者对齐,从而实现模式串的快速移动。而提前将这个前缀(next数组)找出来则是KMP算法的重点。
详细算法原理讲解建议参看视频:【天勤公开课】KMP算法易懂版
这里直接附上代码:
// KMP算法主体逻辑。str是主串,pattern是模式串
public static int kmp(String str, String pattern) {
//预处理,生成next数组
int[] next = getNexts(pattern);
int j = 0;
//主循环,遍历主串字符
for (int i = 0; i < str.length(); i++) {
while (j > 0 && str.charAt(i) != pattern.charAt(j)) {
//遇到坏字符时,查询next数组并改变模式串的起点
j = next[j];
}
if (str.charAt(i) == pattern.charAt(j)) {
j++;
}
if (j == pattern.length()) {
//匹配成功,返回下标
return i - pattern.length() + 1;
}
}
return -1;
}
// 生成Next数组
private static int[] getNexts(String pattern) {
int[] next = new int[pattern.length()];
int j = 0;
for (int i=2; i<pattern.length(); i++) {
while (j != 0 && pattern.charAt(j) != pattern.charAt(i-1)) {
//从next[i+1]的求解回溯到 next[j]
j = next[j];
}
if (pattern.charAt(j) == pattern.charAt(i-1)) {
j++;
}
next[i] = j;
}
return next;
}
public static void main(String[] args) {
String str = "ATGTGAGCTGGTGTGTGCFAA";
String pattern = "GTGTGCF";
int index = kmp(str, pattern);
System.out.println("首次出现位置:" + index);
}