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

  并对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;
    }
  
}

 
 

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

  1. 字符串匹配Boyer-Moore算法:文本编辑器中的查找功能是如何实现的?---这应该讲的最容易懂的文章了!

    关于字符串匹配算法有很多,之前我有讲过一篇 KMP 匹配算法:图解字符串匹配 KMP 算法,不懂 kmp 的建议看下,写的还不错,这个算法虽然很牛逼,但在实际中用的并不是特别多.至于选择哪一种字符串匹 ...

  2. 字符串匹配&Rabin-Karp算法讲解

    问题描述: Rabin-Karp的预处理时间是O(m),匹配时间O( ( n - m + 1 ) m )既然与朴素算法的匹配时间一样,而且还多了一些预处理时间,那为什么我们还要学习这个算法呢?虽然Ra ...

  3. 字符串匹配--Karp-Rabin算法

    主要特征 1.使用hash函数 2.预处理阶段时间复杂度O(m),常量空间 3.查找阶段时间复杂度O(mn) 4.期望运行时间:O(n+m) 本文地址:http://www.cnblogs.com/a ...

  4. 字符串匹配(hash算法)

    hash函数对大家来说不陌生吧 ? 而这次我们就用hash函数来实现字符串匹配. 首先我们会想一下二进制数. 对于任意一个二进制数,我们将它化为10进制的数的方法如下(以二进制数1101101为例): ...

  5. leetcode28 strstr kmp bm sunday

    字符串匹配有KMP,BM,SUNDAY算法. 可见(https://leetcode-cn.com/problems/implement-strstr/solution/c5chong-jie-fa- ...

  6. 字符串匹配 扩展KMP BM&Sunday

    复杂度都是O(n) 扩展1:BM算法 KMP的匹配是从模式串的开头开始匹配的,而1977年,德克萨斯大学的Robert S. Boyer教授和J Strother Moore教授发明了一种新的字符串匹 ...

  7. 【数据结构与算法】字符串匹配(Rabin-Karp 算法和KMP 算法)

    Rabin-Karp 算法 概念 用于在 一个字符串 中查找 另外一个字符串 出现的位置. 与暴力法不同,基本原理就是比较字符串的 哈希码 ( HashCode ) , 快速的确定子字符串是否等于被查 ...

  8. 字符串匹配问题(暴力,kmp)

    对于字符串的匹配问题,现在自己能够掌握的就只有两种方法, 第一种就是我们常用的暴力匹配法,那什么是暴力匹配法呢? 假设我们现在有一个文本串和一个模式串,我们现在要找出模式串在文本串的哪个位置. 文本串 ...

  9. 字符串匹配--manacher算法模板

    manacher算法主要是处理字符串中关于回文串的问题的,它可以在 O(n) 的时间处理出以字符串中每一个字符为中心的回文串半径,由于将原字符串处理成两倍长度的新串,在每两个字符之间加入一个特定的特殊 ...

随机推荐

  1. Celery -- 分布式任务队列 及实例

    Celery 使用场景及实例 Celery介绍和基本使用 在项目中如何使用celery 启用多个workers Celery 定时任务 与django结合 通过django配置celery perio ...

  2. python中内建函数isinstance的用法

    语法:isinstance(object,type) 作用:来判断一个对象是否是一个已知的类型. 其第一个参数(object)为对象,第二个参数(type)为类型名(int...)或类型名的一个列表( ...

  3. QT开发环境搭建

    一.Qt发展史 1991年,由奇趣科技开发的跨平台C++图形用户界面应用程序开发框架: 2008年,Nokia从Trolltech公司收购Qt, 并增加LGPL的授权模式: 2011年,Digia从N ...

  4. rtp header

    rtp协议基于udp传输,流媒体音视频数据被封装在rtp中,通过rtp协议进行实时的传输. 一.rtp协议头格式 The RTP header has a minimum size of 12 byt ...

  5. Ubuntu 16.04 服务器上配置使用 Docker

    Docker基础概念 在使用Docker之前,我们先了解下几个Docker的核心概念 Docker Daemon Docker引擎,就是运行在后台的一个守护进程,在我们启动它之后,我们就可以通过Doc ...

  6. docker OCI runtime

    Open Container Initiative(OCI)目前有2个标准:runtime-spec以及image-spec.前者规定了如何运行解压过的filesystem bundle.OCI规定了 ...

  7. 详解C#特性和反射(二)

    使用反射(Reflection)使得程序在运行过程中可以动态的获取对象或类型的类型信息,然后调用该类型的方法和构造函数,或访问和修改该类型的字段和属性:可以通过晚期绑定技术动态的创建类型的实例:可以获 ...

  8. docker-dockerfile使用

    使用 centos基础镜像, 构建dockerfile-ngix 简单说, 就是把需要做的东西写下来, 然后build的时候, 自动运行 一般包含:  基础镜像信息 维护者信息 镜像操作指令 容器启动 ...

  9. 比較C struct 與 C# unsafe struct内存分佈

    昨晚在群裏無意間看到一個朋友有一個需求.他是在C裏面將兩個結構體(HeadStruct,BodyStruct)的内存數據直接通過socket send發給C#寫的服務端來處理.當然他之前所使用的需求基 ...

  10. 版本管理(二)之Git和GitHub的连接和使用

    首先需要注册登录GitHub:https://github.com 然后 ①:下载Git 先从Git官网,由于我的系统是64位的所以选择64-bit Git for Windows Setup htt ...