查看原文
其他

漫画:如何优化 “字符串匹配算法”?

脚本之家 2021-06-30

The following article is from 程序员小灰 Author 小灰

  脚本之家

你与百万开发者在一起


说起“字符串匹配”,恐怕算得上是计算机领域应用最多的功能之一,为了满足这一需求,聪明的计算机科学家们发明了许多巧妙的算法。


在上一篇漫画中,我们介绍了BF算法RK算法,没看过的小伙伴可以先补补课:


漫画:什么是字符串匹配算法?


今天,我们来介绍一种性能大大优化的字符串匹配算法。






BF算法是如何工作的?


正如同它的全称BruteForce一样,BF算法使用简单粗暴的方式,对主串和模式串进行逐个字符的比较。


比如给定主串和模式串如下:



它们的比较过程是什么样的呢?


第一轮,模式串和主串的第一个等长子串比较,发现第0位字符一致,第1位字符一致,第2位字符不一致:





第二轮,模式串向后挪动一位,和主串的第二个等长子串比较,发现第0位字符不一致:



第三轮,模式串继续向后挪动一位,和主串的第三个等长子串比较,发现第0位字符不一致:



以此类推,一直到第N轮:



当模式串挪动到某个合适位置,逐个字符比较,发现每一位字符都是匹配时,比较结束:









坏字符规则


“坏字符” 是什么意思?就是指模式串和子串当中不匹配的字符。


还以上面的字符串为例,当模式串和主串的第一个等长子串比较时,子串的最后一个字符T就是坏字符:





当检测到第一个坏字符之后,我们有必要让模式串一位一位向后挪动和比较吗?并不需要。


因为只有式串与符T对齐的位置也是字符T的情况下两者才有匹配的可能。


不难发现,模式串的第1位字符也是T,这样一来我们就可以对模式串做一次“乾坤大挪移”,直接把模式串当中的字符T和主串的坏字符对齐,进行下一轮的比较:



坏字符的位置越靠右,下一轮模式串的挪动跨度就可能越长,节省的比较次数也就越多。这就是BM算法从右向左检测的好处。


接下来,我们继续逐个字符比较,发现右侧的G、C、G都是一致的,但主串当中的字符A,是又一个坏字符:




我们按照刚才的方式,找到模式串的第2位字符也是A,于是我们把模式串的字符A和主串中的坏字符对齐,进行下一轮比较:



接下来,我们继续逐个字符比较,这次发现全部字符都是匹配的,比较公正完成:






  1. //在模式串中,查找index下标之前的字符是否和坏字符匹配

  2. private static int findCharacter(String pattern, char badCharacter, int index) {

  3. for(int i= index-1; i>=0; i--){

  4. if(pattern.charAt(i) == badCharacter){

  5. return i;

  6. }

  7. }

  8. //模式串不存在该字符,返回-1

  9. return -1;

  10. }



  1. public static int boyerMoore(String str, String pattern) {

  2. int strLength = str.length();

  3. int patternLength = pattern.length();

  4. //模式串的起始位置

  5. int start = 0;

  6. while (start <= strLength - patternLength) {

  7. int i;

  8. //从后向前,逐个字符比较

  9. for (i = patternLength - 1; i >= 0; i--) {

  10. if (str.charAt(start+i) != pattern.charAt(i))

  11. //发现坏字符,跳出比较,i记录了坏字符的位置

  12. break;

  13. }

  14. if (i < 0) {

  15. //匹配成功,返回第一次匹配的下标位置

  16. return start;

  17. }

  18. //寻找坏字符在模式串中的对应

  19. int charIndex = findCharacter(pattern, str.charAt(start+i), i);

  20. //计算坏字符产生的位移

  21. int bcOffset = charIndex>=0 ? i-charIndex : i+1;

  22. start += bcOffset;

  23. }

  24. return -1;

  25. }



  1. public static void main(String[] args) {

  2. String str = "GTTATAGCTGGTAGCGGCGAA";

  3. String pattern = "GTAGCGGCG";

  4. int index = boyerMoore(str, pattern);

  5. System.out.println("首次出现位置:" + index);

  6. }




好后缀规则


“好后缀” 又是什么意思?就是指模式串和子串当中相匹配的后缀。


让我们看一组新的例子:



对于上面的例子,如何我们继续使用“坏字符规则”,会有怎样的效果呢?


从后向前比对字符,我们发现后面三个字符都是匹配的,到了第四个字符的时候,发现坏字符G:



接下来我们在模式串找到了对应的字符G,但是按照坏字符规则,模式串仅仅能够向后挪动一位:



这时候坏字符规则显然并没有起到作用,为了能真正减少比较次数,轮到我们的好后缀规则出场了。由于好后缀规则的实现细节比坏字符规则要难理解得多,所以我们这里只介绍一个大概思路:



我们回到第一轮的比较过程,发现主串和模式串都有共同的后缀“GCG”,这就是所谓的“好后缀”。


如果模式串其他位置也包含与“GCG”相同的片段,那么我们就可以挪动模式串,让这个片段和好后缀对齐,进行下一轮的比较:



显然,在这个例子中,采用好后缀规则能够让模式串向后移动更多位,节省了更多无谓的比较。












- END -



更多精彩


在公众号后台对话框输入以下关键词

查看更多优质内容!


女朋友 | 大数据 | 运维 | 书单 | 算法

大数据 | JavaScript | Python | 黑客

AI | 人工智能 | 5G | 区块链

机器学习 | 数学 | 送书

●  人人都欠微软一个正版?

●  脚本之家粉丝福利,请查看!

●  程序员怒打产品经理,这个需求做不了

● 致敬经典:Linux/UNIX必读书单推荐给你

 一个故事讲完CPU的工作原理

● 终于有人把 Nginx 说清楚了,图文详解!

    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存