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

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

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

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

1 BF算法: 
暴力检索法是最好想到的算法,也最好实现,在情况简单的情况下可以直接使用: 

首先将原字符串和子串左端对齐,逐一比较;如果第一个字符不能匹配,则子串向后移动一位继续比较;如果第一个字符匹配,则继续比较后续字符,直至全部匹配。 
时间复杂度:O(MN)

2 RK算法: 
RK算法是对BF算法的一个改进:在BF算法中,每一个字符都需要进行比较,并且当我们发现首字符匹配时仍然需要比较剩余的所有字符。而在RK算法中,就尝试只进行一次比较来判定两者是否相等。 
RK算法也可以进行多模式匹配,在论文查重等实际应用中一般都是使用此算法。 
 
首先计算子串的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算法那样向后移动一位,而是按照事先计算好的“部分匹配表”中记载的位数来移动,节省了大量时间。这里我借用一下阮一峰大神的例子来讲解: 
 
首先,原字符串和子串左端对齐,比较第一个字符,发现不相等,子串向后移动,直到子串的第一个字符能和原字符串匹配。 
 
当A匹配上之后,接着匹配后续的字符,直至原字符串和子串出现不相等的字符为止。 
 
此时如果按照BF算法计算,是将子串整体向后移动一位接着从头比较;按照KMP算法的思想,既然已经比较过了“ABCDAB”,就要利用这个信息;所以针对子串,计算出了“部分匹配表”如下(具体如何计算后面会说,这个先介绍整个流程): 
 
刚才已经匹配的位数为6,最后一个匹配的字符为“B”,查表得知“B”对应的部分匹配值为2,那么移动的位数按照如下公式计算: 
移动位数 = 已匹配的位数 - 最后一个匹配字符的部分匹配值 
那么6 - 2 = 4,子串向后移动4位,到下面这张图: 
 
因为空格和“C”不匹配,已匹配位数为2,“B”对应部分匹配值为0,所以子串向后移动2-0=2位。 
 
空格和“A”不匹配,已匹配位数为0,子串向后移动一位。 
 
逐个比较,直到发现“C”与“D”不匹配,已匹配位数为6,“B”对应部分匹配值为2,6-2=4,子串向后移动4位。 
 
逐个比较,直到全部匹配,返回结果。 
下面说明一下“部分匹配表”如何计算,“部分匹配值”是指字符串前缀和后缀所共有元素的长度。前缀是指除最后一个字符外,一个字符串全部头部组合;后缀是指除第一个字符外,一个字符串全部尾部组合。以”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教授的例子来讲解: 
 
首先,原字符串和子串左端对齐,但是从尾部开始比较,就是首先比较“S”和“E”,这是一个十分巧妙的做法,如果字符串不匹配的话,只需要这一次比较就可以确定。 
在BM算法中,当每次发现当前字符不匹配的时候,我们就需要寻找一下子串中是否有这个字符;比如当前“S”和“E”不匹配,那我们需要寻找一下子串当中是否存在“S”。发现子串当中并不存在,那我们将子串整体向后移动到原字符串中“S”的下一个位置(但是如果子串中存在原字符串当前字符肿么办呢,我们后面再说): 
 
我们接着从尾部开始比较,发现“P”和“E”不匹配,那我们查找一下子串当中是否存在“P”,发现存在,那我们就把子串移动到两个“P”对齐的位置: 
 
已然从尾部开始比较,“E”匹配,“L”匹配,“P”匹配,“M”匹配,“I”和“A”不匹配!那我们就接着寻找一下子串当前是否出现了原字符串中的字符,我们发现子串中第一个“E”和原字符串中的字符可以对应,那直接将子串移动到两个“E”对应的位置: 
 
接着从尾部比较,发现“P”和“E”不匹配,那么检查一下子串当中是否出现了“P”,发现存在,那么移动子串到两个“P”对应: 
 
从尾部开始,逐个匹配,发现全部能匹配上,匹配成功~ 
时间复杂度:最差情况O(MN),最好情况O(N)

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

时间: 2024-10-03 05:09:51

字符串匹配常见算法(BF,RK,KMP,BM,Sunday)的相关文章

一个关于字符串匹配的算法题目

有这样一个算法题目 假设这有一个各种字母组成的字符串,假设这还有另外一个字符串,而且这个字符串里的字母数相对少一些.从算法是讲, 什么方法能最快的查出所有短字符串里的字母在长字符串里都有? 比如,如果是下面两个字符串: String 1: ABCDEFGHLMNOPQRS String 2: DCGSRQPOM 答案是true,所有在string2里的字母string1也都有.如果是下面两个字符串: String 1: ABCDEFGHLMNOPQRS String 2: DCGSRQPOZ 答

字符串匹配——朴素算法、KMP算法

字符串匹配(string match)是在实际工程中经常会碰到的问题,通常其输入是原字符串(String)和子串(又称模式,Pattern)组成,输出为子串在原字符串中的首次出现的位置.通常精确的字符串搜索算法包括朴素搜索算法,KMP, BM(Boyer Moore), sunday, robin-karp 以及 bitap.下面分析朴素搜索算法和KMP这两种方法并给出其实现.假设原字符T串长度N,子串P长度为M. 1.NAIVE-STRING-MATCHING. 朴素算法,该方法又称暴力搜索,

字符串匹配暴力算法 与 字符串匹配的KMP算法

声明:先看一下阮一峰的网络日志关于字符串的KMP算法的讲解.本文图片均引用于这篇日志. 在先前的笔试中遇到了关于字符串匹配的问题,一时脑袋卡壳没写好算法.现在就来分析分析 暴力算法和KMP算法各自原理,以及代码实现,之间差异,并且总结一下好算法的一般思路. =========================================================================== 各自原理: 暴力算法: 1. 我们把长的字符串做为一个文本字符串,命名为strText,把

字符串匹配问题 ---- 算法导论读书笔记

字符串匹配是一个很常见的问题,可以扩展为模式的识别,解决字符串问题的思想被广泛地应用.介绍四种解决该问题的办法,包括:最朴素的遍历法,Rabin-Karp算法,自动机机匹配,Knuth-Morris-Pratt算法即耳熟能详的KMP. 在一开始,先对时间复杂度做出一个总扩(从大到小):[1]朴素法:O( (n-m+1)m ):[2]Rabin-Karp:预处理:O(m),匹配:最坏O( (n-m+1)m ),但是平均和实际中比这个好得多:[3]自动机:预处理O(m|Σ|),匹配O(n):[4]K

字符串匹配系列算法

问题描述: 在匹配串中寻找模式串,如: 匹配串:THIS IS A SIMPLE EXAMPLE 模式串(搜索词):EXAMPLE 算法1:Brute Force算法(蛮力搜索法) 首先将匹配串和模式串左对齐,然后从左向右一个一个进行比较,如果不成功则模式串向右移动一个单位. 算法2:Karp Rabin算法 模式串:pattern="pappar",长度记为pattern_len 文本串:text="pappappapparrassanuaragh" 预备阶段就是

字符串匹配(hash算法)

hash函数对大家来说不陌生吧 ? 而这次我们就用hash函数来实现字符串匹配. 首先我们会想一下二进制数. 对于任意一个二进制数,我们将它化为10进制的数的方法如下(以二进制数1101101为例): hash用的也是一样的原理,为每一个前缀(也可以后缀,笔者习惯1 base,所以喜欢用前缀来计算,Hash[i] = Hash[i - 1] * x + s[i](其中1 < i <= n,Hash[0] = 0). 一般地, 而对于l - r区间的hash值,则为: 但是如果n很大呢?那样不是

字符串匹配的算法(暴力算法和KMP算法)

学习字符串匹配算法有一段时间了,不过还是有点迷糊,虽然了解算法过程,但是在编码的时候还是会有些迷糊. 先把写的程序放在这里,以后有时间再来翻着看看吧! #include<iostream> #include<string> using namespace std; int KMPfind(char* s, char* p); void GetNext(char* p, int next[]); int ViolentMatch(char* s, char* p); int main

字符串匹配的算法

      char str[10] = "china";       char *p = str;       print  sizeof(str) = %lu, sizeof(p) = %lu  ,   sizeof(str), sizeof(p);       => sizeof(str) = 10;  //是str的空间大小       => sizeof(p) = 4;   // 内存地址的大小       => unsight long sizeof(){

字符串匹配——朴素算法Brute-Force(布鲁特-福斯算法)

布鲁特-福斯算法 简单的模式匹配算法是一种带回溯的匹配算法. 一.算法思想 从主串S的第pos个字符开始,和模式串T的第一个字符开始比较,如果相等就继续比较后续字符,如果不等,则从(回溯到)主串S的第pos+1个字符开始重新和模式串T进行比较,直到模式串T中的每一个字符和主串的每一个连续字符子序列全部相等,则称匹配成功,返回和T第一个字符相等的字符在主串S中的位置:或者主串中没有和模式串相等的字符序列,则称匹配不成功. 二.算法实现代码 设置i,j,start三个指示器: i--指向主串S中当前