【KMP算法】高效字符串匹配BF,RK,BM,KMP算法合集

判断字符串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;
    }
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值