判断字符串B是否在字符串A中包含?
这道题是典型的字符串匹配问题,本质想考的是KMP算法,我在字节面试也遇到了。需要掌握理解并能手撕出来。
BF(Brute Force暴力)算法
这也是我面试说的算法,很明显效率太低
具体实现就是控制i,j指针;遇到相同的就同时后移,知道完全匹配。
但当极端情况;
aaaaaaaaaab
aab时,会产生大量无用比较浪费时间。
RK算法
BF算法只是简单粗暴的将所有字符依次边角,而RK算法比较的是两个字符串的哈希值。
每一个字符串都可以通过某种哈希算法,转换成一个整型数,这个整型数就是hashcode
hashcode=hash(string)
显然,对于逐个字符的比较,仅比较两个字符串的hashcode要更容易
hashcode算法:转换26进制数
例如:abc=1*(26^2)+2*26+3
这样可以大幅减少哈希冲突,缺点是计算量较大,而且很有可能出现超出整型范围的情况,需要对计算结果进行取模
过程:
1、计算模式串的hashcode
2、生成与模式串等长的hashcode
3、比较两个hashcode
4、依次向后遍历
5、如果相等,然后进行精细比较,比较每一位是否相同
向后遍历的时候没有必要每次都重新计算窗口中所有字符串的值,而是可以减去第一个,加上后一个即可
public static int rabinKarp(String str,String pattern){
int m=str.length();
int n=pattern.length();
int patternCode=hash(pattern);
int strCode=hash(str.substring(0,n));
for(int i=0;i<m-n+1;i++){
if(strCode==patternCode&&compareString(i,str,pattern)){
return i;
}
if(i<m-n){
strCode=nextHash(str,strCode,i,n);
}
}
return -1;
}
private static int hash(String str){
int hashcode=0;
for (int i=0;i<str.length();i++){
hashcode+=str.charAt(i)-'a';
}
return hashcode;
}
时间复杂度降到O(n)
缺点是哈希冲突,如果哈希冲突太多,退化成BF算法。
BM 算法
为了解决BF算法中很多无用比较的缺点;
BM算法利用好后缀和坏后缀
坏字符规则:
模式串中和子串当中不匹配的字符;
GTTA T AGCTGGTAGCGGCGAA
GTAGCGGCG
这里的T就是坏字符
BM算法的检测顺序相反,是从字符串的最右测检测,反向检测
算法规定,只有模式串中的字符和最坏字符的位置对齐的情况下字符相同,两者才有匹配的可能。
所以经过第一轮移动;将T与T对其
GTTA T AGCTGGTAGCGGCGAA
GTAGCGGCG
第二轮最坏后缀为A;
GTTA T AGCTGGTAGCGGCGAA
GTAGCGGCG
这样字符串经过比较就可以完全匹配了。
如果坏后缀不在模式字符串中,将模式字符串移动到坏后缀的后一个位置
好后缀规则
例:
CTGGGCGAGCGGAA
GCGAGCG
上述例子如果继续使用坏字符规则;
坏字符G,每次调整时只能调整一格,并不能有效的减少次数;
此时就轮到好字符规则了;
后三个GCG都匹配,所以是好后缀,
在模式串中找到与好后缀相同的字符,移到好后缀对应的位置;
CTGGGCGAGCGGAA
GCGAGCG
这样可以移动更多位,节省了无谓的比较
如果模式串中不存在好后缀,找好后缀的后缀,依次挪动
我们可以根据计算挪动的长度来选择使用好后缀还是坏后缀
KMP算法
和BM算法相似,KMP算法也在试图减少无谓的字符比较。为了实现这一点,KMP算法把专注点放在了已匹配的前缀
GTGTGAGCTGGTGTGTGCFAA
GTGTGCF
和BF一样,把主串和模式串的首位对齐,从左到右对逐个字符进行比较
第一轮
发现第六个字符是不匹配的,是一个坏字符;
有效利用已匹配的前缀GTGTG
发现和模式串中前三个有交集GTG
GTGTGAGCTGGTGTGTGCFAA
GTGTGCF
主串中的GTG称为最长可匹配后缀子串
模式串中的GTG称为最长可匹配前缀子串
在下一轮比较时,只有把这两个相同的片段对齐,才有可能出现匹配。
第二轮
发现第四个字符是坏字符,这时匹配前缀缩短成了GTG,最长可匹配后缀子串变成了G
GTGTGAGCTGGTGTGTGCFAA
GTGTGCF
依次向后走,如果没有可匹配的,则跳入坏后缀的后边
如何找最长匹配后缀和最长可匹配前缀
那我们怎么找呢,如果每一轮都重新遍历显然效率比较低;
这里解决的办法是事先缓存在一个集合中,用的时候从集合里面取出;
这个集合被称为next数组,如何生成是KMP算法中最大的难点;
推荐博客KMP
借用文中的图
这个图是列出了所有的可能性
细品;
当没有前缀的时候,自然没有最长可匹配前缀子串;
当已匹配的前缀是G,此时相当于G和G对齐,没有移动的空间;
当已匹配的前缀是GT,此时因为GT没有移动的空间,模式字符串是G开头,GT移动的时候不可能遇到第二个G,除非不移动;
当已匹配的前缀是GTG,此时有两个G,可匹配的就是第二个G,元素值为1;
当已匹配的前缀是GTGT,此时也是有两个G,可匹配的字符是第二个G后面的所有字符,元素值为2;
当已匹配的前缀是GTGTG,同理,元素值为3;
当已匹配的前缀是GTGTGC,假设最长可匹配后缀为GTGC,或GC,此时怎么移动都不可能与之匹配,所以已匹配前缀还有最重要的特点,对称(GTGT)回文(GTGTG)
GTGTGAGCTGGTGTGTGCAAA
GTGTGCF
如何事先生成呢?
依据模式串生成,如果逐个比较,效率会很低;
我们可以采用动态规划的方法,
首先初始化next[0]=0;next[1]=1;
- GT------i=2;j=0;此时pattern[j]!=pattern[i-1];最长可匹配字符串仍然不存在;所以next[2]=0;
- GTG------i=3;j=0;此时pattern[j]==pattern[i-1];第一次出现所以是1,next[3]=next[2]+1;
- GTGT-------i=4;j=1;此时pattern[j]==pattern[i-1],next[4]=next[3]+1;
- GTGTG-------i=5;j=2;此时pattern[j]==pattern[i-1],next[5]=next[4]+1;
- GTGTGC-------i=6;j=3;这时我们不能通过next【5】推导next【6】了,因为字符C前面有两段重复的GTG,那么需要转化问题;
- 将GTGTGC最长可匹配前缀子串问题变为计算GTGC最长可匹配前缀子串问题,回溯到j=next[j],pattern[j]!=pattern[i-1];则继续回溯;
- 将问题转换成GC最长可匹配前缀子串问题,回溯到j=0,此时pattern[j]!=pattern[i-1],所以next[6]=0;
总结:
1、对模式串预处理,生成next数组;
2、进入主循环,遍历主串
2.1比较主串和模式串的字符
2.2如果发现坏字符,查询next数组,得到匹配前缀所对应的最长可匹配前缀子串,移动模式串到对应位置
2.3如果当前字符串匹配,继续循环
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;
}