字符串匹配常见算法(BF,RK,KMP,BM,Sunday)

  今日了解了一下字符串匹配的各种方法。

  并对sundaysearch算法实现并且单元。

字符串匹配算法,是在实际工程中经常遇到的问题,也是各大公司笔试面试的常考题目。此算法通常输入为原字符串(string)和子串(pattern),要求返回子串在原字符串中首次出现的位置。比如原字符串为“ABCDEFG”,子串为“DEF”,则算法返回3。常见的算法包括:BF(Brute Force,暴力检索)、RK(Robin-Karp,哈希检索)、KMP(教科书上最常见算法)、BM(Boyer Moore)、Sunday等,下面详细介绍。

1 BF算法: 
暴力检索法是最好想到的算法,也最好实现,在情况简单的情况下可以直接使用: 
《字符串匹配常见算法(BF,RK,KMP,BM,Sunday)》
首先将原字符串和子串左端对齐,逐一比较;如果第一个字符不能匹配,则子串向后移动一位继续比较;如果第一个字符匹配,则继续比较后续字符,直至全部匹配。 
时间复杂度:O(MN)

2 RK算法: 
RK算法是对BF算法的一个改进:在BF算法中,每一个字符都需要进行比较,并且当我们发现首字符匹配时仍然需要比较剩余的所有字符。而在RK算法中,就尝试只进行一次比较来判定两者是否相等。 
RK算法也可以进行多模式匹配,在论文查重等实际应用中一般都是使用此算法。 
《字符串匹配常见算法(BF,RK,KMP,BM,Sunday)》 
首先计算子串的HASH值,之后分别取原字符串中子串长度的字符串计算HASH值,比较两者是否相等:如果HASH值不同,则两者必定不匹配,如果相同,由于哈希冲突存在,也需要按照BF算法再次判定。 
按照此例子,首先计算子串“DEF”HASH值为Hd,之后从原字符串中依次取长度为3的字符串“ABC”、“BCD”、“CDE”、“DEF”计算HASH值,分别为Ha、Hb、Hc、Hd,当Hd相等时,仍然要比较一次子串“DEF”和原字符串“DEF”是否一致。 
时间复杂度:O(MN)(实际应用中往往较快,期望时间为O(M+N))

3 KMP算法: 
字符串匹配最经典算法之一,各大教科书上的看家绝学,曾被投票选为当今世界最伟大的十大算法之一;但是晦涩难懂,并且十分难以实现,希望我下面的讲解能让你理解这个算法。 
KMP算法在开始的时候,也是将原字符串和子串左端对齐,逐一比较,但是当出现不匹配的字符时,KMP算法不是向BF算法那样向后移动一位,而是按照事先计算好的“部分匹配表”中记载的位数来移动,节省了大量时间。这里我借用一下阮一峰大神的例子来讲解: 
《字符串匹配常见算法(BF,RK,KMP,BM,Sunday)》 
首先,原字符串和子串左端对齐,比较第一个字符,发现不相等,子串向后移动,直到子串的第一个字符能和原字符串匹配。 
《字符串匹配常见算法(BF,RK,KMP,BM,Sunday)》 
当A匹配上之后,接着匹配后续的字符,直至原字符串和子串出现不相等的字符为止。 
《字符串匹配常见算法(BF,RK,KMP,BM,Sunday)》 
此时如果按照BF算法计算,是将子串整体向后移动一位接着从头比较;按照KMP算法的思想,既然已经比较过了“ABCDAB”,就要利用这个信息;所以针对子串,计算出了“部分匹配表”如下(具体如何计算后面会说,这个先介绍整个流程): 
《字符串匹配常见算法(BF,RK,KMP,BM,Sunday)》 
刚才已经匹配的位数为6,最后一个匹配的字符为“B”,查表得知“B”对应的部分匹配值为2,那么移动的位数按照如下公式计算: 
移动位数 = 已匹配的位数 – 最后一个匹配字符的部分匹配值 
那么6 – 2 = 4,子串向后移动4位,到下面这张图: 
《字符串匹配常见算法(BF,RK,KMP,BM,Sunday)》 
因为空格和“C”不匹配,已匹配位数为2,“B”对应部分匹配值为0,所以子串向后移动2-0=2位。 
《字符串匹配常见算法(BF,RK,KMP,BM,Sunday)》 
空格和“A”不匹配,已匹配位数为0,子串向后移动一位。 
《字符串匹配常见算法(BF,RK,KMP,BM,Sunday)》 
逐个比较,直到发现“C”与“D”不匹配,已匹配位数为6,“B”对应部分匹配值为2,6-2=4,子串向后移动4位。 
《字符串匹配常见算法(BF,RK,KMP,BM,Sunday)》 
逐个比较,直到全部匹配,返回结果。 
下面说明一下“部分匹配表”如何计算,“部分匹配值”是指字符串前缀和后缀所共有元素的长度。前缀是指除最后一个字符外,一个字符串全部头部组合;后缀是指除第一个字符外,一个字符串全部尾部组合。以”ABCDABD”为例: 
“AB”的前缀为[A],后缀为[B],共有元素的长度为0; 
“ABC”的前缀为[A, AB],后缀为[BC, C],共有元素的长度0; 
“ABCD”的前缀为[A, AB, ABC],后缀为[BCD, CD, D],共有元素的长度为0; 
“ABCDA”的前缀为[A, AB, ABC, ABCD],后缀为[BCDA, CDA, DA, A],共有元素为”A”,长度为1; 
“ABCDAB”的前缀为[A, AB, ABC, ABCD, ABCDA],后缀为[BCDAB, CDAB, DAB, AB, B],共有元素为”AB”,长度为2; 
“ABCDABD”的前缀为[A, AB, ABC, ABCD, ABCDA, ABCDAB],后缀为[BCDABD, CDABD, DABD, ABD, BD, D],共有元素的长度为0。 
在计算“部分匹配表”时,一般使用DP(动态规划)算法来计算(表示为next数组):

        int* next = new int[needle.length()]; next[0] = 0; int k = 0; for (int i = 1; i < needle.length(); i++) { while (k > 0 && needle[i] != needle[k]) { k = next[k - 1]; } if (needle[i] == needle[k]) { k++; } next[i] = k; }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

时间复杂度:O(N)

4 BM算法: 
在本科的时候,我一直认为KMP算法是最好的字符串匹配算法,直到后来我遇到了BM算法。BM算法的执行效率要比KMP算法快3-5倍左右,并且十分容易理解。各种记事本的“查找”功能(CTRL + F)一般都是采用的此算法。 
网上所有讲述这个算法的帖子都是以传统的“好字符规则”和“坏字符规则”来讲述的,但是个人感觉其实这样不容易理解,我总结了另外一套简单的算法规则: 
我们拿这个算法的发明人Moore教授的例子来讲解: 
《字符串匹配常见算法(BF,RK,KMP,BM,Sunday)》 
首先,原字符串和子串左端对齐,但是从尾部开始比较,就是首先比较“S”和“E”,这是一个十分巧妙的做法,如果字符串不匹配的话,只需要这一次比较就可以确定。 
在BM算法中,当每次发现当前字符不匹配的时候,我们就需要寻找一下子串中是否有这个字符;比如当前“S”和“E”不匹配,那我们需要寻找一下子串当中是否存在“S”。发现子串当中并不存在,那我们将子串整体向后移动到原字符串中“S”的下一个位置(但是如果子串中存在原字符串当前字符肿么办呢,我们后面再说): 
《字符串匹配常见算法(BF,RK,KMP,BM,Sunday)》 
我们接着从尾部开始比较,发现“P”和“E”不匹配,那我们查找一下子串当中是否存在“P”,发现存在,那我们就把子串移动到两个“P”对齐的位置: 
《字符串匹配常见算法(BF,RK,KMP,BM,Sunday)》 
已然从尾部开始比较,“E”匹配,“L”匹配,“P”匹配,“M”匹配,“I”和“A”不匹配!那我们就接着寻找一下子串当前是否出现了原字符串中的字符,我们发现子串中第一个“E”和原字符串中的字符可以对应,那直接将子串移动到两个“E”对应的位置: 
《字符串匹配常见算法(BF,RK,KMP,BM,Sunday)》 
接着从尾部比较,发现“P”和“E”不匹配,那么检查一下子串当中是否出现了“P”,发现存在,那么移动子串到两个“P”对应: 
《字符串匹配常见算法(BF,RK,KMP,BM,Sunday)》 
从尾部开始,逐个匹配,发现全部能匹配上,匹配成功~ 
时间复杂度:最差情况O(MN),最好情况O(N)

5 Sunday算法: 
后来,我又发现了一种比BM算法还要快,而且更容易理解的算法,就是这个Sunday算法: 
《字符串匹配常见算法(BF,RK,KMP,BM,Sunday)》 
首先原字符串和子串左端对齐,发现“T”与“E”不匹配之后,检测原字符串中下一个字符(在这个例子中是“IS”后面的那个空格)是否在子串中出现,如果出现移动子串将两者对齐,如果没有出现则直接将子串移动到下一个位置。这里空格没有在子串中出现,移动子串到空格的下一个位置“A”: 
《字符串匹配常见算法(BF,RK,KMP,BM,Sunday)》 
发现“A”与“E”不匹配,但是原字符串中下一个字符“E”在子串中出现了,第一个字符和最后一个字符都有出现,那么首先移动子串靠后的字符与原字符串对齐: 
《字符串匹配常见算法(BF,RK,KMP,BM,Sunday)》 
发现空格和“E”不匹配,原字符串中下一个字符“空格”也没有在子串中出现,所以直接移动子串到空格的下一个字符“E”: 
《字符串匹配常见算法(BF,RK,KMP,BM,Sunday)》 
这样从头开始逐个匹配,匹配成功! 
时间复杂度:最差情况O(MN),最好情况O(N)

 

sunday算法代码如下:

 import java.util.HashMap;

import java.util.LinkedList;

import java.util.List;

import java.util.Map;

public class SundySearch {

    String text = null;

    String pattern = null;

    int currentPos = 0;

    /**

     * 匹配后的子串第一个字符位置列表

     */

    List<Integer> matchedPosList = new LinkedList<Integer>();

    /**

     * 匹配字符的Map,记录改匹配字符串有哪些char并且每个char最后出现的位移

     */

    Map<Character, Integer> map = new HashMap<Character, Integer>();

    public SundySearch(String text, String pattern) {

        this.text = text;

        this.pattern = pattern;

        this.initMap();

    };

    /**

     * Sunday匹配时,用来存储Pattern中每个字符最后一次出现的位置,从左到右的顺序

     */

    private void initMap() {

        for (int i = 0; i < pattern.length(); i++) {

            this.map.put(pattern.charAt(i), i);

        }

    }

    /**

     * 普通的字符串递归匹配,匹配失败就前进一位

     */

    public List<Integer> normalMatch() {

        //匹配失败,继续往下走

        if (!matchFromSpecialPos(currentPos)) {

            currentPos += 1;

            if ((text.length() – currentPos) < pattern.length()) {

                return matchedPosList;

            }

            normalMatch();

        } else {

            //匹配成功,记录位置

            matchedPosList.add(currentPos);

            currentPos += 1;

            normalMatch();

        }

        return matchedPosList;

    }

    /**

     * Sunday匹配,假定Text中的K字符的位置为:当前偏移量+Pattern字符串长度+1

     */

    public List<Integer> sundayMatch() {

        // 如果没有匹配成功

        if (!matchFromSpecialPos(currentPos)) {

            // 如果Text中K字符没有在Pattern字符串中出现,则跳过整个Pattern字符串长度

            if ((currentPos + pattern.length() + 1) < text.length()

                    && !map.containsKey(text.charAt(currentPos + pattern.length() + 1))) {

                currentPos += pattern.length();

            }else {

                // 如果Text中K字符在Pattern字符串中出现,则将Text中K字符的位置和Pattern字符串中的最后一次出现K字符的位置对齐

                if ((currentPos + pattern.length() + 1) > text.length()) {

                    currentPos += 1;

                } else {

                    currentPos += pattern.length() – (Integer) map.get(text.charAt(currentPos + pattern.length()));

                }

            }

            // 匹配完成,返回全部匹配成功的初始位移

            if ((text.length() – currentPos) < pattern.length()) {

                return matchedPosList;

            }

            sundayMatch();

        }else {

            // 匹配成功前进一位然后再次匹配

            matchedPosList.add(currentPos);

            currentPos += 1;

            sundayMatch();

        }

        return matchedPosList;

    }

    /**

     * 检查从Text的指定偏移量开始的子串是否和Pattern匹配

     */

    public boolean matchFromSpecialPos(int pos) {

        if ((text.length()-pos) < pattern.length()) {

            return false;

        }

        for (int i = 0; i < pattern.length(); i++) {

            if (text.charAt(pos + i) == pattern.charAt(i)) {

                if (i == (pattern.length()-1)) {

                    return true;

                }

                continue;

            } else {

                break;

            }

        }

        return false;

    }

  

}  
测试sunday算法代码如下:  import java.util.HashMap;

import java.util.LinkedList;

import java.util.List;

import java.util.Map;

public class SundySearch {

    String text = null;

    String pattern = null;

    int currentPos = 0;

    /**

     * 匹配后的子串第一个字符位置列表

     */

    List<Integer> matchedPosList = new LinkedList<Integer>();

    /**

     * 匹配字符的Map,记录改匹配字符串有哪些char并且每个char最后出现的位移

     */

    Map<Character, Integer> map = new HashMap<Character, Integer>();

    public SundySearch(String text, String pattern) {

        this.text = text;

        this.pattern = pattern;

        this.initMap();

    };

    /**

     * Sunday匹配时,用来存储Pattern中每个字符最后一次出现的位置,从左到右的顺序

     */

    private void initMap() {

        for (int i = 0; i < pattern.length(); i++) {

            this.map.put(pattern.charAt(i), i);

        }

    }

    /**

     * 普通的字符串递归匹配,匹配失败就前进一位

     */

    public List<Integer> normalMatch() {

        //匹配失败,继续往下走

        if (!matchFromSpecialPos(currentPos)) {

            currentPos += 1;

            if ((text.length() – currentPos) < pattern.length()) {

                return matchedPosList;

            }

            normalMatch();

        } else {

            //匹配成功,记录位置

            matchedPosList.add(currentPos);

            currentPos += 1;

            normalMatch();

        }

        return matchedPosList;

    }

    /**

     * Sunday匹配,假定Text中的K字符的位置为:当前偏移量+Pattern字符串长度+1

     */

    public List<Integer> sundayMatch() {

        // 如果没有匹配成功

        if (!matchFromSpecialPos(currentPos)) {

            // 如果Text中K字符没有在Pattern字符串中出现,则跳过整个Pattern字符串长度

            if ((currentPos + pattern.length() + 1) < text.length()

                    && !map.containsKey(text.charAt(currentPos + pattern.length() + 1))) {

                currentPos += pattern.length();

            }else {

                // 如果Text中K字符在Pattern字符串中出现,则将Text中K字符的位置和Pattern字符串中的最后一次出现K字符的位置对齐

                if ((currentPos + pattern.length() + 1) > text.length()) {

                    currentPos += 1;

                } else {

                    currentPos += pattern.length() – (Integer) map.get(text.charAt(currentPos + pattern.length()));

                }

            }

            // 匹配完成,返回全部匹配成功的初始位移

            if ((text.length() – currentPos) < pattern.length()) {

                return matchedPosList;

            }

            sundayMatch();

        }else {

            // 匹配成功前进一位然后再次匹配

            matchedPosList.add(currentPos);

            currentPos += 1;

            sundayMatch();

        }

        return matchedPosList;

    }

    /**

     * 检查从Text的指定偏移量开始的子串是否和Pattern匹配

     */

    public boolean matchFromSpecialPos(int pos) {

        if ((text.length()-pos) < pattern.length()) {

            return false;

        }

        for (int i = 0; i < pattern.length(); i++) {

            if (text.charAt(pos + i) == pattern.charAt(i)) {

                if (i == (pattern.length()-1)) {

                    return true;

                }

                continue;

            } else {

                break;

            }

        }

        return false;

    }

  

}    

    原文作者:sunwengang
    原文地址: https://www.cnblogs.com/1996swg/p/7116017.html
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞

发表评论

电子邮件地址不会被公开。 必填项已用*标注