串匹配【C++ 实现】

目录

1.BF

(1)算法思想——双指针

(2)算法实现

(3)性能分析

(4)算法改进——KMP引入

 2.KMP介绍

(1)串的最大相同前后缀

(2)最大相同前后缀在KMP中的作用

(3)next数组

(4)next数组的求解

3.KMP实现

(1)数据结构

(2)获取next数组

(3)匹配

(4)KMP改进

4.总结


1.BF
(1)算法思想——双指针

分别利用指针i、j指示主串master和模式串pattern中当前待比较的字符位置;

从master[0]和pattern[0]开始比较,若相等则继续逐个比较后续字符(i++,j++),若不相等则从主串的下一个字符(master[i-j+1])起再重新与模式串(pattern[0])比较。以此类推,直至整个模式串匹配完成,即匹配成功,否则匹配失败。

(2)算法实现
int find(const std::string &master, const std::string &pattern){
    int i=0,j=0;
    while(i<master.size() && j<pattern.size()){
        if(master[i] == pattern[j]){
            i++;
            j++;
        }else{
            i=i-j+1; // 此时pattern[0]~pattern[j-1]已成功匹配,共j个字符,所以i需要回退j-1个字符
            j=0;
        }
    }
    
    if(j==pattern.size()){
        return i-j; // 此时已匹配j==pattern.size()个字符,所以i比失配时还要多回退一个字符
    }
    return -1;
}
(3)性能分析

考察:master="a string searching example consisting of simple text"、pattern="sting"

可以得到find(master,pattern)=32

其中,"while"语句的循环次数为41,即【find(master,pattern)+pattern.size()+4】,也就是说,除master中红色字符比较了2次以外,其余字符均只比较了一次。


考察:master="00000000001"、pattern="001"

可以得到find(master,pattern)=8

其中,"while"语句的循环次数为27,即【find(master,pattern)+pattern.size()+2+7*2】,也就是说,除master中红色字符比较了2次、蓝色字符比较了3次以外,其余字符均只比较了一次。


结论:

BF的算法在主串中存在多个和模式串“部分匹配”的子串的情况时,会引起指针i多次回溯,因此最坏时间复杂度T(n)=O(master.size()*pattern.size())。但在一般情况下,BF的时间复杂度近似于O(master.size()+pattern.size()),因此至今仍被采用

(4)算法改进——KMP引入

KMP算法可以在O(master.size()+pattern.size())内完成串匹配操作,其对BF算法的改进仅体现在失配时的策略上,具体为:

  • 不回溯主串指针i
  • 模式串指针j可以在O(1)内回溯到指定位置(仅改进后的KMP可以保证,后面会说)
 2.KMP介绍
(1)串的最大相同前后缀

对于某个串而言:

  • 前缀是从头部开始、不包含串尾字符的所有连续子串
  • 后缀是从尾部开始、不包含串首字符的所有连续子串
  • 最大相同前后缀就是所有前缀和后缀中,相等的且最长的

举个例子:

对于str="abcabc"

所有前缀:"a"、"ab"、"abc"、"abca"、"abcab"

所有后缀:"c"、"bc"、"abc"、"cabc"、"bcabc"

最大相同前后缀:"abc"

(2)最大相同前后缀在KMP中的作用

最大相同前后缀的长度可以作为失配时模式串指针回溯的参考


举个例子:

设在某次失配时已成功匹配j个字符。

① 若j>0:

对于已成功匹配的部分,设其最大相同前后缀的长度为k:

  • pattern[0]~pattern[j-1]==master[i-j]~master[i-1]
  • pattern[0]~pattern[k-1]==pattern[j-k]~pattern[j-1]
  • master[0]~master[k-1]==master[i-k]~master[i-1]

因此我们只需要比较pattern[k]与master[i]即可(即令j=k,i不变)。

② 若j==0:

表示模式串第一个字符就匹配失败,故主串指针i不用回溯,还需前进一个位置,而模式串指针j不变。

(3)next数组

从左往右遍历模式串,对每个字符,都将其前面串的最大相同前后缀的长度求出来,然后依次存到一个一维数组里面,就构成了KMP的核心——next数组。下面我们分情况讨论next数组是如何求解的。

(4)next数组的求解

设模式串中当前字符为pattern[j]、pattern[0]~pattern[j-1]的最大相同前后缀长度为k。


① if(pattern[j]==pattern[k])

  • pattern[0]~pattern[k-1]==pattern[j-k]~pattern[j-1](next[j]=k);
  • pattern[0]~pattern[k-1]+pattern[k]==pattern[j-k]~pattern[j-1]+pattern[j]

故有:next[j+1]=k+1


② if(pattern[j]!=pattern[k])

设next[k]=r:

  • pattern[0]~pattern[k-1]==pattern[j-k]~pattern[j-1](next[j]=k);
  • pattern[0]~pattern[r-1]==pattern[k-r]~pattern[k-1]
  • pattern[j-k]~pattern[j-k+r-1]==pattern[j-r]~pattern[j-1]

因此,当pattern[j]!=pattern[k]时,需要转去判断pattern[j]与pattern[r](即pattern[next[k]])是否相等,若相等则转①,若不相等则继续转②。


③ 特别的

  • 当j==0时,pattern[0]前面没有任何字符,此时令next[0]=-1
  • 当j==1时,串pattern[0]没有相同的前后缀,此时next[1]=0
3.KMP实现
(1)数据结构
#include <iostream>
#include <vector>
#include <string>

class KMP{
public:
    static std::vector<int> getNext(const::string &pattern);
    static int find(const std::string &master, const std::string &pattern);
};
(2)获取next数组
static std::vector<int> KMP::getNext(const string &pattern){
    std::vector<int> next(pattern.size()+1);
    next[0]=-1;// pattern[0]前面没有串
    int j=1,k=next[j];
    
    while(j<pattern.size()){
        if(k==-1 || pattern[j]==pattern[k]){
            next[j+1]=k+1;
            j++;
            k++;
        }else{
            k=next[k];// 转去比较pattern[j]和pattern[next[k]]
        }
    }

    return next;
}
(3)匹配
static int KMP::find(const std::string &master,const std::string pattern) {
    vector<int> next=getNext(pattern);

    int i=0,j=0;
    while(i<master.size() && j<pattern.size()){
        if(pattern[j]==master[i]){
            i++;
            j++;
        }else if(j==0){
            i++;
        }else{
            j=next[j];
        }
    }

    if(j==pattern.size()){
        return i-j;
    }
    return -1;
}
(4)KMP改进

缺陷:

上面的函数getNext()中的next[j+1]=k+1存在缺陷,若有next[j+1]==j && pattern[j+1]==pattern[j],也即pattern[0]~pattern[j]中所有的字符均相等。此时若pattern[j+1]失配,势必导致模式串指针回溯到pattern[next[j+1]],也即pattern[j],然后发现pattern[j]与回溯前的pattern[j+1]一致,这就是重复比较。在这种情况下,模式串指针要经过j次重复比较才能回溯到指定位置pattern[0]。


举个例子:

master="aaabaaabaaaab"、pattern="aaaab":

  • 按next数组的求解过程,得next={-1,0,1,2,3,0}
  • 按KMP匹配过程,当pattern[3]!=master[3]时,要令j=next[j]=next[3]=2,然后比较pattern[2]和master[3]。而pattern[2]!=master[3],故又要令j=next[j]=next[2]=1,然后比较pattern[1]和master[3]。而pattern[1]!=master[3],故又要令j=next[j]=next[1]=0,然后比较pattern[0]和master[3]。然后i++,比较pattern[0]和master[4]......

在这种情况下,模式串指针j并未在O(1)内回溯到指定位置pattern[0]。


改进:

由上述分析可知,每当求得一个字符的next值时,我们只需要判断它与它前面串中的所有字符是否都相等,如果是则直接重置它的next值为0即可,这样可以使它在失配时直接回溯到指定位置pattern[0]。体现在代码中,也就是每当我们求得next[j+1]=k+1时,需要进一步判断next[j+1]==j && pattern[j+1]=pattern[j]是否成立,若成立则需令next[j+1]=0。

改进后的getNext()如下:

static std::vector<int> KMP::getNext(const string &pattern){
    std::vector<int> next(pattern.size()+1);
    next[0]=-1;// pattern[0]前面没有串
    int j=1,k=next[j];
    
    while(j<pattern.size()){
        if(k==-1 || pattern[j]==pattern[k]){
            next[j+1]=k+1;

            // 改进
            if(next[j+1]==j && pattern[j+1]==pattern[j]){
                next[j+1]=0;
            }

            j++;
            k++;
        }else{
            k=next[k];// 转去比较pattern[j]和pattern[next[k]]
        }
    }

    return next;
}
4.总结
  1. 虽然BF的时间复杂度T(n)=O(master.size()*pattern.size()),但在一般情况下,其实际执行时间近似于O(master.size()+pattern.size()),因此至今仍被采用
  2. KMP可以在T(n)=O(master.size()+pattern.size())内完成串匹配,这是以额外的空间S(n)=O(pattern.size()+1)为代价的
  3. KMP与BF的区别仅在于失配时的策略上,故仅当模式串与主串之间存在许多“部分匹配”的情况下才显得KMP比BF快。在“部分匹配”几乎不存在的情况下,BF的整体性能要优于KMP(两者时间开销近似,但空间开销差距大)
  4. KMP的最大特点是主串指针不需回溯,这对处理从外设输入的庞大文件很有效,可以一边输入一边匹配

相关推荐

  1. 匹配C++ 实现

    2024-04-03 20:12:01       41 阅读
  2. C#实现字符串模糊匹配

    2024-04-03 20:12:01       24 阅读
  3. C/C++】C语言实现

    2024-04-03 20:12:01       27 阅读
  4. C语言栈实现就近匹配原则

    2024-04-03 20:12:01       56 阅读
  5. 【数据结构与算法】暴力匹配-C语言版

    2024-04-03 20:12:01       53 阅读
  6. 2 的模式匹配算法(KMP)

    2024-04-03 20:12:01       30 阅读

最近更新

  1. docker php8.1+nginx base 镜像 dockerfile 配置

    2024-04-03 20:12:01       94 阅读
  2. Could not load dynamic library ‘cudart64_100.dll‘

    2024-04-03 20:12:01       101 阅读
  3. 在Django里面运行非项目文件

    2024-04-03 20:12:01       82 阅读
  4. Python语言-面向对象

    2024-04-03 20:12:01       91 阅读

热门阅读

  1. Linux的Shell基础知识总结

    2024-04-03 20:12:01       35 阅读
  2. redis简介

    2024-04-03 20:12:01       39 阅读
  3. Vue-------自定义指令

    2024-04-03 20:12:01       39 阅读
  4. git diff

    2024-04-03 20:12:01       37 阅读
  5. linux ldd依赖拷贝

    2024-04-03 20:12:01       40 阅读
  6. LeetCode 36

    2024-04-03 20:12:01       35 阅读
  7. U3D开发中Json管理器的常用思路

    2024-04-03 20:12:01       36 阅读
  8. MySQL面试题系列-2

    2024-04-03 20:12:01       36 阅读
  9. Mysql中的那些索引

    2024-04-03 20:12:01       40 阅读
  10. c++ 死锁检测与内存泄露

    2024-04-03 20:12:01       28 阅读
  11. 自定义注解实现对实体类的字段进行校验

    2024-04-03 20:12:01       37 阅读