[算法系列之十四]字符串匹配之Morris-Pratt字符串搜索算法

前言

我们前面已经看到,蛮力字符串匹配算法和Rabin-Karp字符串匹配算法均非有效算法。不过,为了改进某种算法,首先需要详细理解其基本原理。我们已经知道,暴力字符串匹配的速度缓慢,并已尝试使用Rabin-Karp中的一个散列函数对其进行改进。问题是,Rabin-Karp的复杂度与强力字符串匹配相同,均为O(mn)。

我们显然需要采用一种不同方法,但为了提出这种不同方法,先来看看暴力字符串匹配有什么不妥之处。事实上,再深入地研究一下它的基本原理,就能找到问题的答案了。

在暴力匹配算法中,需要检查文本串中的每个字符是否与模式串的第一个字符匹配。如果匹配,就顺次比较模式串的第二个字符,判断是否与文本串的下一字符匹配。问题在于,当出现失配时,我们必须要在文本串中回退若干位置。嗯,这种方法事实上是无法优化的。

在暴力字符串匹配算法中,若出现失配,必须回退,并匹配已经匹配过的字符!

我们可以从上图可以看出问题所在:一旦出现失配,必须回退,从文本串中一个已经考察过的位置开始比较。在我们的例子中,我们已经检查了第一、二、三、四个字符,此时模式串与文本串之间出现失配,于是……于是我们就不得不得回退回去,从文本串的第二个字符重新开始比较。

这一过程显然没有任何作用,因为我们已经知道模式串从字符“a”起始,并且在位置1与位置3之间没有这一字符。那我们如何改善这种不必要的重复呢?

概述

James H. Morris和Vaughan Pratt在1977年回答了这一问题,并且对自己的算法进行了介绍,这种算法会跳过大量无用比较,所以其效率高于暴力字符串匹配。我们来详细地研究一下。唯一事情就是:利用在对模式串与可能匹配进行对比期间收集的信息(The only thing is to use the information gathered during the comparisons of the pattern and a possible match),如下图所示。

Morris-Pratt向前移动到下一可能匹配位置,从而跳过一些不必要的比较!

我们首先需要做的就是必须对模式串进行预处理,以获取后续匹配的可能位置。下一步,开始查找可能的匹配位置,在发生失配的情况下,我们可以准确地知道应当跳转到何处,从而跳过那些没有任何用处的比较。

生成后续对比位置表格

这是Morris-Pratt算法中最富有技巧性的地方,也是这种算法如何克服暴力字符串匹配算法缺陷的重要步骤。让我们来看几张图片。

很显然,如果模式串中仅包含不同字符,在发生失配时,我们应当将文本串中的下一字符与模式串的第一字符进行比较!

然而,当模式串中存在重复字符情况时,如果在该字符之后出现失配,则必须从这一重复字符开始查找可能的匹配,如下图所示。

如果模式串中包含重复字符,则“下一位置”表格会稍有不同!

最后,如果文本串中的重复字符不止1个,“下一个”表格将会给出其位置。

有了这个包含“后续”可能位置的表格之后,就可以开始在文本串中查找模式串了。

实现

Morris-Pratt算法的实现并不困难。首先,必须对模式串进行预处理,然后执行搜索。原文是使用PHP实现的,在这我们使用c++实现。

/*--------------------------------
*   日期:2015-02-05
*   作者:SJF0115
*   题目: 字符串匹配之Morris-Pratt匹配算法
*   博客:
------------------------------------*/
#include <iostream>
using namespace std;

// 预处理
void PreprocessMorrisPratt(string patttern,int nextTable[]){
    int i = 0;
    int j = nextTable[0] = -1;
    int size = patttern.size();
    while(i < size){
        while(j > -1 && patttern[i] != patttern[j]){
            j = nextTable[j];
        }//while
        nextTable[++i] = ++j;
    }//while
}

int SubString(string text,string pattern){
    int m = pattern.size();
    int n = text.size();
    int nextTable[m+1];
    // 预处理
    PreprocessMorrisPratt(pattern,nextTable);
    int i = 0,j = 0;
    while( j < n){
        while(i > -1 && pattern[i] != text[j]){
            i = nextTable[i];
        }//while
        i++;
        j++;
        if(i >= m){
           return j  - i;
        }//if
    }//while
    return -1;
}

int main(){
    string text("Lorem ipsum dolor sit amet, consectetur adipiscing elit. Quisque eleifend nisi viverra ipsum elementum porttitor quis at justo. Aliquam ligula felis, dignissim sit amet lobortis eget, lacinia ac augue. Quisque nec est elit, nec ultricies magna. Ut mi libero, dictum sit amet mollis non, aliquam et augue!");
    string pattern("mollis");
    int result = SubString(text,pattern);
    // 275
    cout<<"下标位置->"<<result<<endl;
    return 0;
}

复杂度

这一算法需要一定的时间和空间进行预处理。模式串的预处理可以在O(m)内完成,其中m为模式串的长度,而搜索本身需要O(m+n)。好消息是预处理过程只需要完成一次,然后就可以根据需要执行任意次搜索了!

下面的图表给出了5字母模式串的O(n+m)复杂度,并将其与O(nm)进行对比。

应用

优点

其搜索复杂度为O(m+n),快于强力算法和Rabin-Karp算法

其实现相当容易

缺点

需要额外的空间与时间-O(m)进行预处理

可以稍加优化(Knuth-Morris-Pratt)

结语

显然,这一算法非常有用,因为它以一种非常雅致的方式对强力匹配算法进行了改进。另一方面,我们必须知道还有诸如Boyer-Moore算法等更快速的字符串查找算法。不过,Morris-Pratt算法在许多情况下都非常有用,所以理解其基本原理后可能会非常便利。

原文链接

Computer Algorithms: Morris-Pratt String Searching

相关贴子

[算法系列之十二]字符串匹配之蛮力匹配

[算法系列之十三]Rabin-Karp字符串查找算法

时间: 2024-12-27 15:54:15

[算法系列之十四]字符串匹配之Morris-Pratt字符串搜索算法的相关文章

[算法系列之十二]字符串匹配之蛮力匹配

引言 字符串匹配是数据库开发和文字处理软件的关键.幸运的是所有现代编程语言和字符串库函数,帮助我们的日常工作.不过理解他们的原理还是比较重要的. 字符串算法主要可以分为几类.字符串匹配就是其中之一.当我们提到字符串匹配算法,最基本的方法就是所谓的蛮力解法,这意味着我们需要检查每一个文本串中的字符是否和匹配串相匹配.一般来说我们有文本串和一个匹配串(通常匹配串短于文本串).我们需要做的就是回答这个匹配串是否出现在文本串中. 概述 字符串蛮力匹配法的原理非常简单.我们必须检查匹配串的第一个字符与文本

[算法系列之二十六]字符串匹配之KMP算法

一 简介 KMP算法是一种改进的字符串匹配算法,由D.E.Knuth与V.R.Pratt和J.H.Morris同时发现,因此人们称它为克努特-莫里斯-普拉特操作(简称KMP算法).KMP算法的关键是利用匹配失败后的信息,尽量减少模式串与主串的匹配次数以达到快速匹配的目的. 二 基于部分匹配表的KMP算法 举例来说,有一个字符串"BBC ABCDAB ABCDABCDABDE",我想知道,里面是否包含搜索串"ABCDABD"? 步骤1:字符串"BBC ABC

quick-cocos2d-x 学习系列之十四 测试用例

quick-cocos2d-x 学习系列之十四 测试用例 定义变量,创建13个场景名字 local items = { "framework.helper", "framework.native", "framework.display", "framework.crypto", "framework.network", "framework.luabinding", "fra

[算法系列之十八]海量数据处理之BitMap

一:简介 所谓的BitMap就是用一个bit位来标记某个元素对应的Value, 而Key即是该元素.由于采用了bit为单位来存储数据,因此在存储空间方面,可以大大节省. 二:基本思想 我们用一个具体的例子来讲解,假设我们要对0-7内的5个元素(4,7,2,5,3)排序(这里假设这些元素没有重复).那么我们就可以采用BitMap的方法来达到排序的目的.要表示8个数,我们就只需要8个bit(1Bytes). (1)首先我们开辟1字节(8bit)的空间,将这些空间的所有bit位都置为0,如下图: (2

Chrome浏览器扩展开发系列之十四:本地消息机制Native messagin

Chrome浏览器扩展开发系列之十四:本地消息机制Native messaging 2016-11-24 09:36 114人阅读 评论(0) 收藏 举报  分类: PPAPI(27)  通过将浏览器所在客户端的本地应用注册为Chrome浏览器扩展的"本地消息主机(native messaging host)",Chrome浏览器扩展还可以与客户端本地应用之间收发消息. 客户端的本地应用注册为Chrome浏览器扩展的"本地消息主机"之后,Chrome浏览器会在独立的

Chrome浏览器扩展开发系列之十四

Chrome浏览器扩展开发系列之十四:本地消息机制Native messaging 时间:2015-10-08 16:17:59      阅读:1361      评论:0      收藏:0      [点我收藏+] 通过将浏览器所在客户端的本地应用注册为Chrome浏览器扩展的"本地消息主机(native messaging host)",Chrome浏览器扩展还可以与客户端本地应用之间收发消息. 客户端的本地应用注册为Chrome浏览器扩展的"本地消息主机"

C++语言笔记系列之十四——继承后的访问权限

1.析构函数不继承:派生类对象在析构时,基类析构函数的调用顺序与构造函数相反. 注:派生类对象建立时要调用基类构造函数,派生类对象删除时要调用基类析构,顺序与构造函数严格相反. 2.例子 example 1 #include <iostream.h> #include <math.h> class Point { public: Point(double a, double b, doule c) { x = a; y = b; z = c; } double Getx() {re

hbase源码系列(十四)Compact和Split

先上一张图讲一下Compaction和Split的关系,这样会比较直观一些. Compaction把多个MemStore flush出来的StoreFile合并成一个文件,而Split则是把过大的文件Split成两个. 之前在Delete的时候,我们知道它其实并没有真正删除数据的,那总不能一直不删吧,下面我们就介绍一下它删除数据的过程,它就是Compaction. 在讲源码之前,先说一下它的分类和作用. Compaction主要起到如下几个作用: 1)合并文件 2)清除删除.过期.多余版本的数据

【算法】利用有限自动机进行字符串匹配

1102. Strange Dialog Time Limit: 1.0 second Memory Limit: 16 MB One entity named "one" tells with his friend "puton" and their conversation is interesting. "One" can say words "out" and "output", besides h