字符串匹配算法之KMP算法和BM算法

2024-09-02 17:18
文章标签 算法 字符串 匹配 kmp bm

本文主要是介绍字符串匹配算法之KMP算法和BM算法,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

[尊重原创]-原文链接在这里->http://blogread.cn/it/article/3975?f=wb

本文主要介绍KMP算法和BM算法,它们分别是前缀匹配和后缀匹配的经典算法。所谓前缀匹配是指:模式串和母串的比较从左到右,模式串的移动也是从左到右;所谓后缀匹配是指:模式串和母串的的比较从右到左,模式串的移动从左到右。看得出来前缀匹配和后缀匹配的区别就仅仅在于比较的顺序不同。下文分别从最简单的前缀蛮力匹配算法和后缀蛮力匹配算法入手,详细的介绍KMP算法和BM算法以及它们的实现。

KMP算法

    首先来看一下前缀蛮力匹配算法的代码(以下代码从linux源码string.h中抠出),模式串和母串的比较是从左到右进行(strncmp()),如果找不到和模式串相同的子串,则从左到右移动模式串,距离为1(s++)。

char * strstr(register const char *s, register const char *wanted)
{register const size_t len = strlen(wanted);if (len == 0) return (char *)s;while (*s != *wanted || strncmp(s, wanted, len))if (*s++ == \'\0\')return (char *)NULL;return (char *)s;
}

    KMP算法中的KMP分别是指三个人名:Knuth、Morris、Pratt,其本质也是前缀匹配算法,对比前缀蛮力匹配算法,区别在于它会动态调整每次模式串的移动距离,而不仅仅是加一,从而加快匹配过程。下图通过一个直观的例子展示前缀蛮力匹配算法和KMP算法的区别,前文提过,这二者唯一的不同在于模式串移动距离。

     

    上图中,前缀蛮力匹配算法发现匹配不上,就向右移动距离1,而KMP算法根据已经比较过的前缀信息,了解到应该移动距离为2;换句话说针对母串的下一个匹配字符,KMP算法了解它下回应该匹配模式串的哪个位置,比如上图中,针对母串的第i+1个字符,KMP算法了解它应该匹配模式串的第k+1个字符。为什么会是这样,这是因为母串的子串T[i-k, i]=aba,而模式串的子串P[0,k]=aba,这二者正好相等。所以模式串应该移动到这个位置,从而让母串的第i+1个字符和模式串的第k+1个字符继续比较。

    那k值又是如何寻找?请注意上图中,模式串位置j已经匹配上母串的位置i,也就是T[i-k, i] = P[j-k, j]=aba;根据前文的T[i-k, i] = P[0, k] = aba, 从而得出P[0, k] = P[j-k, j] = aba。通过观察发现,就是在模式的子串[0, j]中寻找一个最长前缀[0,k],从而使得[j-k, j] = [0,k];

     于是可以定义一个jump数组,jump[j]=k,表示满足P[0, k] ==P[j-k, j] 的最大k值,或者表述为:如果模式串j+1匹配不上母串的i+1,那跳转到模式串k+1继续比较。有了这个jump数组,就很容易写出kmp算法的伪代码:

j:=0;
for i:=1 to n do
Beginwhile (j>0) and (P[j+1]<>T[i]) do j:=jump[j];[if P[j+1]=T[i] then j:=j+1;if j=m thenBeginwriteln(\'Pattern occurs with shift \',i-m);end;
end;

    KMP算法中jump数组的构建可以通过归纳法来解决,首先确定jump[1]=0;假设jump[j]=k,也就是P[0, k] == P[j-k, k],如果P[j+1] == P[k+1],那么得出[0,k+1] = P[j-k, j+1],从而更加定义得出jump[j+1] = k+1;

     如果P[j+1] != P[k+1],那就接着比较P[j+1] ?= P[k1+1],其中(jump[k] = k1),根据(jump[k]=k1)的定义,P[0,k1] == P[k-k1, k],根据(jump[j]=k)的定义,P[0, k] == P[j-k, k],根据这两个等式,推出P[0, k1] == P[j-k1, j],如果此时P[j+1] == P[k1+1],则得出:jump[j+1] = K1 +1 = jump[k] +1。

     如果P[j+1] != P[K1+1],继续递归比较P[j+1] 和P[jump[jump[k]]+1] …. P[1];

     如果依次比较都不相等,那么jump[j+1] = 0;写成伪代码如下,可以看出其实就是模式串自我匹配的过程。

jump[1]:=0;
j:=0;
for i:=2 to m do
beginwhile (j>0) and (P[j+1]<>P[i]) do j:=jump[j];if P[j+1]=P[i] then  j:=j+1;jump[i]:=j;
end;

    考虑模式串匹配不上母串的最坏情况,前缀蛮力匹配算法的时间复杂度最差是O(n×m),最好是O(n),其中n为母串的长度,m为模式串的长度。KMP算法最差的时间复杂度是O(n);最好的时间复杂度是O(n/m)。

BM算法

    后缀匹配,是指模式串的比较从右到左,模式串的移动也是从左到右的匹配过程,经典的BM算法其实是对后缀蛮力匹配算法的改进。所以还是先从最简单的后缀蛮力匹配算法开始。下面直接给出伪代码,注意这一行代码:j++;BM算法所做的唯一的事情就是改进了这行代码,即模式串不是每次移动一步,而是根据已经匹配的后缀信息,从而移动更多的距离。

j = 0;
while (j <= strlen(T) - strlen(P)) {for (i = strlen(P) - 1; i >= 0 && P[i] ==T[i + j]; --i)if (i < 0)match;else++j;
}

    为了实现更快移动模式串,BM算法定义了两个规则,好后缀规则和坏字符规则,如下图可以清晰的看出他们的含义。利用好后缀和坏字符可以大大加快模式串的移动距离,不是简单的++j,而是j+=max (shift(好后缀), shift(坏字符))

     

    先来看如何根据坏字符来移动模式串,shift(坏字符)分为两种情况:

  • 坏字符没出现在模式串中,这时可以把模式串移动到坏字符的下一个字符,继续比较,如下图:

        

  • 坏字符出现在模式串中,这时可以把模式串第一个出现的坏字符和母串的坏字符对齐,当然,这样可能造成模式串倒退移动,如下图:

        

         为了用代码来描述上述的两种情况,设计一个数组bmBc[\'k\'],表示坏字符‘k’在模式串中出现的位置距离模式串末尾的最大长度,那么当遇到坏字符的时候,模式串可以移动距离为: shift(坏字符) = bmBc[T[i]]-(m-1-i)。如下图:

         

         数组bmBc的创建非常简单,直接贴出代码如下:

    void preBmBc(char *x, int m, int bmBc[]) {int i;for (i = 0; i < ASIZE; ++i)bmBc[i] = m;for (i = 0; i < m - 1; ++i)bmBc[x[i]] = m - i - 1;
    }
    

        再来看如何根据好后缀规则移动模式串,shift(好后缀)分为三种情况:

  • 模式串中有子串匹配上好后缀,此时移动模式串,让该子串和好后缀对齐即可,如果超过一个子串匹配上好后缀,则选择最靠左边的子串对齐。

        

  • 模式串中没有子串匹配上后后缀,此时需要寻找模式串的一个最长前缀,并让该前缀等于好后缀的后缀,寻找到该前缀后,让该前缀和好后缀对齐即可。

        

  • 模式串中没有子串匹配上后后缀,并且在模式串中找不到最长前缀,让该前缀等于好后缀的后缀。此时,直接移动模式到好后缀的下一个字符。

        

        为了实现好后缀规则,需要定义一个数组suffix[],其中suffix[i] = s 表示以i为边界,与模式串后缀匹配的最大长度,如下图所示,用公式可以描述:满足P[i-s, i] == P[m-s, m]的最大长度s。

         

        构建suffix数组的代码如下:

    suffix[m-1]=m;
    for (i=m-2;i>=0;--i){q=i;while(q>=0&&P[q]==P[m-1-i+q])--q;suffix[i]=i-q;
    }
    

        有了suffix数组,就可以定义bmGs[]数组,bmGs[i] 表示遇到好后缀时,模式串应该移动的距离,其中i表示好后缀前面一个字符的位置(也就是坏字符的位置),构建bmGs数组分为三种情况,分别对应上述的移动模式串的三种情况

  • 模式串中有子串匹配上好后缀

        

  • 模式串中没有子串匹配上好后缀,但找到一个最大前缀

        

  • 模式串中没有子串匹配上好后缀,但找不到一个最大前缀

        

        构建bmGs数组的代码如下:

    void preBmGs(char *x, int m, int bmGs[]) {int i, j, suff[XSIZE];suffixes(x, m, suff);for (i = 0; i < m; ++i)bmGs[i] = m;j = 0;for (i = m - 1; i >= 0; --i)if (suff[i] == i + 1)for (; j < m - 1 - i; ++j)if (bmGs[j] == m)bmGs[j] = m - 1 - i;for (i = 0; i <= m - 2; ++i)bmGs[m - 1 - suff[i]] = m - 1 - i;
    }
    

        再来重写一遍BM算法:

    j = 0;
    while (j <= strlen(T) - strlen(P)) {for (i = strlen(P) - 1; i >= 0 && P[i] ==T[i + j]; --i)if (i < 0)match;elsej += max(bmGs[i], bmBc[T[i]]-(m-1-i));
    }
    

        考虑模式串匹配不上母串的最坏情况,后缀蛮力匹配算法的时间复杂度最差是O(n×m),最好是O(n),其中n为母串的长度,m为模式串的长度。BM算法时间复杂度最好是O(n/(m+1)),最差是多少?留给读者思考。

这篇关于字符串匹配算法之KMP算法和BM算法的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1130579

相关文章

Java中字符串转时间与时间转字符串的操作详解

《Java中字符串转时间与时间转字符串的操作详解》Java的java.time包提供了强大的日期和时间处理功能,通过DateTimeFormatter可以轻松地在日期时间对象和字符串之间进行转换,下面... 目录一、字符串转时间(一)使用预定义格式(二)自定义格式二、时间转字符串(一)使用预定义格式(二)自

Nginx location匹配模式与规则详解

《Nginxlocation匹配模式与规则详解》:本文主要介绍Nginxlocation匹配模式与规则,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录一、环境二、匹配模式1. 精准模式2. 前缀模式(不继续匹配正则)3. 前缀模式(继续匹配正则)4. 正则模式(大

Java 正则表达式URL 匹配与源码全解析

《Java正则表达式URL匹配与源码全解析》在Web应用开发中,我们经常需要对URL进行格式验证,今天我们结合Java的Pattern和Matcher类,深入理解正则表达式在实际应用中... 目录1.正则表达式分解:2. 添加域名匹配 (2)3. 添加路径和查询参数匹配 (3) 4. 最终优化版本5.设计思

Java字符串操作技巧之语法、示例与应用场景分析

《Java字符串操作技巧之语法、示例与应用场景分析》在Java算法题和日常开发中,字符串处理是必备的核心技能,本文全面梳理Java中字符串的常用操作语法,结合代码示例、应用场景和避坑指南,可快速掌握字... 目录引言1. 基础操作1.1 创建字符串1.2 获取长度1.3 访问字符2. 字符串处理2.1 子字

一文详解如何在Python中从字符串中提取部分内容

《一文详解如何在Python中从字符串中提取部分内容》:本文主要介绍如何在Python中从字符串中提取部分内容的相关资料,包括使用正则表达式、Pyparsing库、AST(抽象语法树)、字符串操作... 目录前言解决方案方法一:使用正则表达式方法二:使用 Pyparsing方法三:使用 AST方法四:使用字

Java字符串处理全解析(String、StringBuilder与StringBuffer)

《Java字符串处理全解析(String、StringBuilder与StringBuffer)》:本文主要介绍Java字符串处理全解析(String、StringBuilder与StringBu... 目录Java字符串处理全解析:String、StringBuilder与StringBuffer一、St

openCV中KNN算法的实现

《openCV中KNN算法的实现》KNN算法是一种简单且常用的分类算法,本文主要介绍了openCV中KNN算法的实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的... 目录KNN算法流程使用OpenCV实现KNNOpenCV 是一个开源的跨平台计算机视觉库,它提供了各

Python中使用正则表达式精准匹配IP地址的案例

《Python中使用正则表达式精准匹配IP地址的案例》Python的正则表达式(re模块)是完成这个任务的利器,但你知道怎么写才能准确匹配各种合法的IP地址吗,今天我们就来详细探讨这个问题,感兴趣的朋... 目录为什么需要IP正则表达式?IP地址的基本结构基础正则表达式写法精确匹配0-255的数字验证IP地

MySQL更新某个字段拼接固定字符串的实现

《MySQL更新某个字段拼接固定字符串的实现》在MySQL中,我们经常需要对数据库中的某个字段进行更新操作,本文就来介绍一下MySQL更新某个字段拼接固定字符串的实现,感兴趣的可以了解一下... 目录1. 查看字段当前值2. 更新字段拼接固定字符串3. 验证更新结果mysql更新某个字段拼接固定字符串 -

Java String字符串的常用使用方法

《JavaString字符串的常用使用方法》String是JDK提供的一个类,是引用类型,并不是基本的数据类型,String用于字符串操作,在之前学习c语言的时候,对于一些字符串,会初始化字符数组表... 目录一、什么是String二、如何定义一个String1. 用双引号定义2. 通过构造函数定义三、St