热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

字符串模式匹配算法-BF和KMP详解

1.问题描述字符串模式匹配:串的模式匹配,是求第一个字符串(模式串:str2)在第二个字符串(主串:str1)中的位置。2,简单字符串模式匹配(BF算法)2.1简单匹配思路描述

1.问题描述

字符串模式匹配:串的模式匹配 ,是求第一个字符串(模式串:str2)在第二个字符串(主串:str1)中的位置。

 

2,简单字符串模式匹配(BF算法)

2.1 简单匹配思路描述

简单字符串模式匹配算法,也就是了BF(Brute Force 蛮力,暴力)算法,俗称暴力法。

基本思路:

    • (1) 从主串S指定的字符开始(一般为第1个)和模式串P的第一个字符比较,若相等,则继续逐个比较后续字符,直到P中的每个字符依次和S中的一个连续字符序列相等,则称匹配成功;
    • (2)如果比较过程中有某对字符串不相等,则从主串S的下一个字符起再重新和T的第一个字符比较。如果S中的字符都比完了仍然没有匹配 成功,则称匹配不成功。
简单模式匹配算法--举例:
主串str1:    a b a b c a b c a c b a b
模式串str2:  a b c a c

          (i=0
第一趟匹配:a b a b c a b c a c b a b
          a b c
              (j=2

             (i=1
第二趟匹配:  a b a b c a b c a c b a b
             a 
             (j=0

               (i=2
第三趟匹配: a b a b c a b c a c b a b
               a b c a c
                       (j=4

                (i=3
第四趟匹配:a b a b c a b c a c b a b
                a 
                (j=0

                  (i=4
第五趟匹配:a b a b c a b c a c b a b
                  a  
                  (j=0

                    (i=5      (i=10
第六趟匹配:a b a b c a b c a c b a b
                    a b c a c
                    (j=0      (j=5
              

  

2.2 时间复杂度

设串S和P的长度分别为m,n,则它在最坏情况下的时间复杂度是O(m*n)。BF算法的最坏时间复杂度虽然不好,但它易于理解和编程,在实际应用中,一般还能达到近似于O(m+n)的时间度(最坏情况不是那么容易出现的,RP问题),因此,还在被大量使用。

 

2.3 BF代码实现

#include 
#include 

using namespace std;

int BF_strMatch(vector<char> v1, vector<char> v2) {
    //v1是主串,v2是模式串
    //如果匹配成功,返回子串在主串中的起始位置;否则,返回-1;
    int i = 0, j = 0;
    int n = v1.size(), m = v2.size();
    while (i  m) {
        if (v1[i] == v2[j]) {
            ++i;
            ++j;
        }
        else {
            i = i - j + 2;//通过观察下标变换的关系得出
            j = 1;
        }
    }
    return (j == m) ? (i - m) : -1;
}


void main() {
    vector<char> s1 = { 'a','b','a','b','c','a','b','c','a','c','b','a','b'};
    vector<char> s2 = { 'a','b','c','a','c' };
    cout < endl; }

 

3,经典KMP匹配算法

3.1 KMP算法基本思想

KMP算法可以在O(n+m)的时间数量级上完成串的模式匹配操作。其改进在于:每当一趟匹配过程中出现字符比较不等时,不需回溯 i 指针( i 只增不减),而是利用已经得到的“部分匹配”的结果将模式串向右滑动尽可能远一段距离后,继续进行比较。

 

3.2 KMP算法关键点

KMP算法加速原因:让前面匹配过的信息指导后面。

KMP算法理解的关键点:

  • 1.求解最长公共子串(next数组),只跟模式串 str2 有关,而与主串 str1 无关(有的参考书没有讲解清楚这一点,导致容易混淆);
  • 2.最长公共子串,大多数的参考书称为最长公共前缀,这里我称之为最长公共子串,是为了避免与下面两个概念混淆。这里对最长公共子串的定义:最长前缀和最长后缀的最长公共子串。
    • 最长前缀:从第一个字符的起的连续一串字符,不含最后一个字符
    • 最长后缀:不含第一个字符,从中间某一个字符其到最后一个字符的连续一串字符;

 

例1:对于模式串“a b c a b c d”,求字符d的最长公共子串。
解:设length为字符d最长公共子串的长度,根据定义,length的可能取值为1~5:
                   a b c a b c d length = 1 : a != c
length = 2 : ab != bc
length = 3 : abc == abc
length = 4 : abca != cabc
length = 5 : abcab != bcabc
显然,字符d的最长公共子串的长度为3

例2:对于模式串“a a a a a b”,求字符b的最长公共子串。
同理,length可能的取值为1~4:
length = 1 : a == a
length = 2 : aa == aa
length = 3 : aaa == aaa
length = 4 : aaaa == aaaa
显然,字符b的最长公共子串的长度为4 

 

3.3 求解next数组

next数组的求解,实际是对每个位置找到最长的公共子串:

一般地,对于模式串str2="P0P1P2…Pm-1",长度为m,其next数组的定义:

  1. 当j=0时,即str2中的第一个字符,其前没有字符,人为规定 next[0] = -1;
  2. 当j=1时,即str2中的第二字符,其前只有一个字符,人为规定 next[1] = 0;
  3. 当 2<= j =
  4. Max{ k | 1<= k =0…Pk-1” == "Pj-k…Pj-1"}不空时,next[j] = Max{ k | 1<= k =0…Pk-1” == "Pj-k…Pj-1"},
  5. Max{ k | 1<= k =0…Pk-1” == "Pj-k…Pj-1"}为空时,next[j] = 0 ;

求解next数组的代码:

vector<int> getNext(vector<char> v) {
    //模式串str2 与 模式串str2 (自己跟自己) 做KMP匹配 
    int m = v.size();//v字符串长度
    vector<int> next(m, 0);
    int i = 2;
    int j = 0;

    if (m == 0) return next;
    next[0] = -1;
    if (m == 1) return next;
    next[1] = 0;

    while (i < m) {
        if (v[i - 1] == v[j]) {
            ++j;
            next[i] = j;
            ++i;
        }
        else if(j>0){
            j = next[j];
        }
        else {
            next[i++] = 0;
        }
        
    }
    return next;//放对位置
}

//test
void main() {
    vector<char> s3 = { 'a','b','a','a','b','c','a','c' };
    for (auto c : getNext(s3)) {
        cout <" ";
    }
    cout << endl;
}

3.4 完整的KMP算法代码

#include 
#include 

using namespace std;

int KMP_strMatch(vector<char> v1, vector<char> v2,vector<int> next) {
    //v1是主串,v2是模式串
    //如果匹配成功,返回子串在主串中的起始位置;否则,返回-1;
    int i = 0, j = 0;
    int n = v1.size(), m = v2.size();
    while (i  m) {
        if (v1[i] == v2[j]) {
            ++i;
            ++j;
        }
        else {
            if (j == 0) ++i; //j回到模式串头部还不匹配,i加1
            j = next[j];       //使用next数组提供指导
        }
    }
    return (j == m) ? (i - m) : -1;
}

vector<int> getNext(vector<char> v) {
    //模式串str2 与 模式串str2 (自己跟自己) 做KMP匹配 
    int m = v.size();//v字符串长度
    vector<int> next(m, 0);
    int i = 2;
    int j = 0;

    if (m == 0) return next;
    next[0] = -1;
    if (m == 1) return next;
    next[1] = 0;

    while (i < m) {
        if (v[i - 1] == v[j]) {
            ++j;
            next[i] = j;
            ++i;
        }
        else if(j>0){
            j = next[j];
        }
        else {
            next[i++] = 0;
        }
        
    }
    return next;//放对位置
}


void main() {
    vector<char> s1 = { 'a','b','a','b','c','a','b','c','a','c','b','a','b'};
    vector<char> s2 = { 'a','b','c','a','c' };

    for (auto c : getNext(s2)) {
        cout <" ";
    }
    cout << endl;
    cout < endl;
}

 

 

4,KMP算法进深理解

4.1 BF和KMP执行流程对比

  • 当str1[i] == str2[j]时,操作一样,++i,++j;
  • 当str1[i] != str2[j],即不匹配时:
    • BF:i = i - j + 2, j = 0;
    • KMP:i 不动(不回溯),j = next[j];

 

4.2 next数组要求最大公共子串的原因

        假设中间跳过的部分可以有匹配的,看视频再整理

      

str1:a b k a b a b k a b x str2:a b k a b a b k a b y  //i=10,j=10 不匹配,j=next[10]=5(不是j=0,减少了5次比较),继续比较                a b k a b a b k a b F            //i=10,j=5 不匹配,j=next[5]=2(不是j=0,减少了2次比较),继续比较
a b k a b a b k a b F //i=10,j=2 不匹配,j=next[2]=0(才是j=0,较少了0次比较),继续比较
a b k a b a b k a b F //i=10,j=0 不匹配,++i(此时i才加1),j=0
分析:
(1)加速的原因:减少了不必要的比较次数。
(2)为什么模式串可以向右滑动尽可能远一段距离后,再继续比较:

正如上面的例子:前面都是相等的,到了x与y匹配时才不相等——
(W--------------Q)
[i|-----k---|j |x] //红色部分是滑过的距离,即不必要的比较次数,这里指的i……j之间的位置不可能匹配出str2
(-a-b-k-a-b)(-a-b-k-a-b)

(W--------------Q)
(-a-b-k-a-b)(-a-b-k-a-b)
[0|         |j          |y]

假设从k字符起可以配出str2,那么必然会存在更长的子串(W-------Q)比y位置的最长子串(a-b-k-a-b)更长,如果next数组求解正确,这是不可能。出现矛盾,假设不成立。
因为next数组求的就是每一个字符的最长公共子串。

 

  

参考资料:

1.《数据结构考研复习指导》--王道单科书

2.https://www.cnblogs.com/zzqcn/p/3508442.html#_labelTop  字符串模式匹配算法1 - BF和KMP算法

3. https://www.nowcoder.com/courses/semester/senior 《牛客高级项目课——(牛客网)》--大牛·左程云


推荐阅读
  • 阿里Treebased Deep Match(TDM) 学习笔记及技术发展回顾
    本文介绍了阿里Treebased Deep Match(TDM)的学习笔记,同时回顾了工业界技术发展的几代演进。从基于统计的启发式规则方法到基于内积模型的向量检索方法,再到引入复杂深度学习模型的下一代匹配技术。文章详细解释了基于统计的启发式规则方法和基于内积模型的向量检索方法的原理和应用,并介绍了TDM的背景和优势。最后,文章提到了向量距离和基于向量聚类的索引结构对于加速匹配效率的作用。本文对于理解TDM的学习过程和了解匹配技术的发展具有重要意义。 ... [详细]
  • c语言\n不换行,c语言printf不换行
    本文目录一览:1、C语言不换行输入2、c语言的 ... [详细]
  • 本文介绍了一种划分和计数油田地块的方法。根据给定的条件,通过遍历和DFS算法,将符合条件的地块标记为不符合条件的地块,并进行计数。同时,还介绍了如何判断点是否在给定范围内的方法。 ... [详细]
  • 本文介绍了P1651题目的描述和要求,以及计算能搭建的塔的最大高度的方法。通过动态规划和状压技术,将问题转化为求解差值的问题,并定义了相应的状态。最终得出了计算最大高度的解法。 ... [详细]
  • 本文介绍了解决二叉树层序创建问题的方法。通过使用队列结构体和二叉树结构体,实现了入队和出队操作,并提供了判断队列是否为空的函数。详细介绍了解决该问题的步骤和流程。 ... [详细]
  • 本文介绍了C函数ispunct()的用法及示例代码。ispunct()函数用于检查传递的字符是否是标点符号,如果是标点符号则返回非零值,否则返回零。示例代码演示了如何使用ispunct()函数来判断字符是否为标点符号。 ... [详细]
  • 计算机存储系统的层次结构及其优势
    本文介绍了计算机存储系统的层次结构,包括高速缓存、主存储器和辅助存储器三个层次。通过分层存储数据可以提高程序的执行效率。计算机存储系统的层次结构将各种不同存储容量、存取速度和价格的存储器有机组合成整体,形成可寻址存储空间比主存储器空间大得多的存储整体。由于辅助存储器容量大、价格低,使得整体存储系统的平均价格降低。同时,高速缓存的存取速度可以和CPU的工作速度相匹配,进一步提高程序执行效率。 ... [详细]
  • 《数据结构》学习笔记3——串匹配算法性能评估
    本文主要讨论串匹配算法的性能评估,包括模式匹配、字符种类数量、算法复杂度等内容。通过借助C++中的头文件和库,可以实现对串的匹配操作。其中蛮力算法的复杂度为O(m*n),通过随机取出长度为m的子串作为模式P,在文本T中进行匹配,统计平均复杂度。对于成功和失败的匹配分别进行测试,分析其平均复杂度。详情请参考相关学习资源。 ... [详细]
  • 动态规划算法的基本步骤及最长递增子序列问题详解
    本文详细介绍了动态规划算法的基本步骤,包括划分阶段、选择状态、决策和状态转移方程,并以最长递增子序列问题为例进行了详细解析。动态规划算法的有效性依赖于问题本身所具有的最优子结构性质和子问题重叠性质。通过将子问题的解保存在一个表中,在以后尽可能多地利用这些子问题的解,从而提高算法的效率。 ... [详细]
  • 本文介绍了Java的集合及其实现类,包括数据结构、抽象类和具体实现类的关系,详细介绍了List接口及其实现类ArrayList的基本操作和特点。文章通过提供相关参考文档和链接,帮助读者更好地理解和使用Java的集合类。 ... [详细]
  • Linux环境变量函数getenv、putenv、setenv和unsetenv详解
    本文详细解释了Linux中的环境变量函数getenv、putenv、setenv和unsetenv的用法和功能。通过使用这些函数,可以获取、设置和删除环境变量的值。同时给出了相应的函数原型、参数说明和返回值。通过示例代码演示了如何使用getenv函数获取环境变量的值,并打印出来。 ... [详细]
  • 本文介绍了PE文件结构中的导出表的解析方法,包括获取区段头表、遍历查找所在的区段等步骤。通过该方法可以准确地解析PE文件中的导出表信息。 ... [详细]
  • C++中的三角函数计算及其应用
    本文介绍了C++中的三角函数的计算方法和应用,包括计算余弦、正弦、正切值以及反三角函数求对应的弧度制角度的示例代码。代码中使用了C++的数学库和命名空间,通过赋值和输出语句实现了三角函数的计算和结果显示。通过学习本文,读者可以了解到C++中三角函数的基本用法和应用场景。 ... [详细]
  • 本文介绍了一个题目的解法,通过二分答案来解决问题,但困难在于如何进行检查。文章提供了一种逃逸方式,通过移动最慢的宿管来锁门时跑到更居中的位置,从而使所有合格的寝室都居中。文章还提到可以分开判断两边的情况,并使用前缀和的方式来求出在任意时刻能够到达宿管即将锁门的寝室的人数。最后,文章提到可以改成O(n)的直接枚举来解决问题。 ... [详细]
  • 3.223.28周学习总结中的贪心作业收获及困惑
    本文是对3.223.28周学习总结中的贪心作业进行总结,作者在解题过程中参考了他人的代码,但前提是要先理解题目并有解题思路。作者分享了自己在贪心作业中的收获,同时提到了一道让他困惑的题目,即input details部分引发的疑惑。 ... [详细]
author-avatar
c颖c颖漂亮
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有