KMP(字符串匹配算法)

Posted xuan01

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了KMP(字符串匹配算法)相关的知识,希望对你有一定的参考价值。

主要思想:当出现字符不匹配时,可以利用已经匹配的文本内容,避免从头匹配;

考虑文本串:” aabaabaafa“,模式串 ”aabaaf “, 参考「代码随想录」KMP算法详解 - 找出字符串中第一个匹配项的下标 - 力扣(LeetCode),很详细;

个人理解:1、这个算法是对模式串的要求,模式串有重复的字符,并且前缀和后缀是部分相同的,才能用前缀表记录这个信息;此时这个算法才能做到优化 O(N+M);若模式串没一个重复的相同的,退化为暴力 O(N*M);

     2、前后缀相同理解为:该字符串是否具有  从左端开始的子字符串 与 右端开始的子字符串,是否中心对称,必须是从端开始的;此时将相同部分的长度记录到前缀表;例如,模式串中长度为前5个字符的子串aabaa,最长相同前后缀的长度为2,也就是aa部分;前缀表如下;

 

针对前缀表:当遇到不匹配的字符时,看前一个下标,对应的前缀表的数值是多少,就是为了看有没有能重复利用的已经匹配过的字符串;至此就是KMP的原理。

注意:当遇到不匹配的时候,找到前缀表前一个下标的的值,此时模式串跳转到这个下标处,就要开始判断这个下标了,因为前缀表记录的是相等的长度,而下标是从零开始的,所以长度作为下标正好就是下一个要开始判断是否与文本串匹配的字符;

但是具体实现的时候,多数时候是用next数组,就是前缀表的值都减一;这是为了实现方便;

具体实现

next数组:将next[0 ] = -1; i 代表 前缀终止位置;j 代表后缀终止位置, next[ i] 代表 从零下标当前下标 i 为止 最长相等前后缀长度,就是 j 的值,也就是 对应前缀表的值减一;

void array_Next(int* next, const string& s)
    int j = -1;
    next[0] = -1;
    for(int i = 1; i < s.size();++i)
        while(j >= 0 && s[i] != s[j+1])
            j = next[j];
        
        if(s[i] == s[j+1])
            ++j;
        
        next[i] = j;
    

字符串匹配:下标j 依然为 -1;与next数组保持一致;

整体实现:

#include <bits/stdc++.h>
using namespace std;

/*KMP(快速模式匹配算法)*/

void array_Next(int* next, const string& s)
    int j = -1;
    next[0] = -1;
    for(int i = 1; i < s.size();++i)
        while(j >= 0 && s[i] != s[j+1])
            j = next[j];
        
        if(s[i] == s[j+1])
            ++j; 
        
        next[i] = j;
    

int kmp(string& s,string& t)
    int n = s.size();
    int m = t.size();
    int j = -1;
    int next[m];
    array_Next(next, t);
    for(int i = 0;i < n; ++i)
        while(j >= 0 && s[i] != t[j+1])
            j = next[j];
        
        if(s[i] == t[j+1])
            ++j;
        
        if(j == m - 1)
            return i - m + 1; 
        
    
    return -1;

int main()
    string s = "aabaabaafa", t = "aabaaf";

    cout << "index is : " << kmp(s, t) << endl;

    system("pause");
    return 0;

/*

index is : 3
请按任意键继续. . .

*/

 

字符串匹配算法——KMP算法

1、字符串匹配

字符串匹配是计算机的基本任务之一。

字符串匹配是什么?举例来说,有一个字符串"BBC ABCDAB ABCDABCDABDE",我想知道,里面是否包含另一个字符串"ABCDABD"?

许多算法可以完成这个任务,Knuth-Morris-Pratt算法(简称KMP)是最常用的之一。它以三个发明者命名,起头的那个K就是著名科学家Donald Knuth(《计算机程序设计艺术》的作者)。


2、KMP算法

这个算法不太容易理解,网上有很多解释,但读起来都很费劲。直到读到Jake Boxer的文章,我才真正理解这种算法。下面,我用自己的语言,试图写一篇比较好懂的KMP算法解释。


2.1

技术分享

首先,字符串"BBC ABCDAB ABCDABCDABDE"的第一个字符与搜索词"ABCDABD"的第一个字符,进行比较。因为B与A不匹配,所以搜索词后移一位。


2.2

技术分享

因为B与A不匹配,搜索词再往后移。


2.3

技术分享

就这样,直到字符串有一个字符,与搜索词的第一个字符相同为止。


2.4

技术分享

接着比较字符串和搜索词的下一个字符,还是相同。


2.5

技术分享

直到字符串有一个字符,与搜索词对应的字符不相同为止。


2.6

技术分享

这时,最自然的反应是,将搜索词整个后移一位,再从头逐个比较。这样做虽然可行,但是效率很差,因为你要把"搜索位置"移到已经比较过的位置,重比一遍。


2.7

技术分享

一个基本事实是,当空格与D不匹配时,你其实知道前面六个字符是"ABCDAB"。KMP算法的想法是,设法利用这个已知信息,不要把"搜索位置"移回已经比较过的位置,继续把它向后移,这样就提高了效率。


2.8

技术分享

怎么做到这一点呢?可以针对搜索词,算出一张《部分匹配表》(Partial Match Table)。这张表是如何产生的,后面再介绍,这里只要会用就可以了。


2.9

技术分享

已知空格与D不匹配时,前面六个字符"ABCDAB"是匹配的。查表可知,最后一个匹配字符B对应的"部分匹配值"为2,因此按照下面的公式算出向后移动的位数:

移动位数 = 已匹配的字符数 - 对应的部分匹配值

因为 6 - 2 等于4,所以将搜索词向后移动4位。


2.10

技术分享

因为空格与C不匹配,搜索词还要继续往后移。这时,已匹配的字符数为2("AB"),对应的"部分匹配值"为0。所以,移动位数 = 2 - 0,结果为 2,于是将搜索词向后移2位。


2.11

技术分享


因为空格与A不匹配,继续后移一位。


2.12

技术分享

逐位比较,直到发现C与D不匹配。于是,移动位数 = 6 - 2,继续将搜索词向后移动4位。


2.13

技术分享

逐位比较,直到搜索词的最后一位,发现完全匹配,于是搜索完成。如果还要继续搜索(即找出全部匹配),移动位数 = 7 - 0,再将搜索词向后移动7位,这里就不再重复了。


2.14

技术分享

下面介绍《部分匹配表》是如何产生的。

首先,要了解两个概念:"前缀"和"后缀"。 "前缀"指除了最后一个字符以外,一个字符串的全部头部组合;"后缀"指除了第一个字符以外,一个字符串的全部尾部组合。


2.15

技术分享

"部分匹配值"就是"前缀"和"后缀"的最长的共有元素的长度。以"ABCDABD"为例,

- "A"的前缀和后缀都为空集,共有元素的长度为0;

- "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。


2.16

技术分享

"部分匹配"的实质是,有时候,字符串头部和尾部会有重复。比如,"ABCDAB"之中有两个"AB",那么它的"部分匹配值"就是2("AB"的长度)。搜索词移动的时候,第一个"AB"向后移动4位(字符串长度-部分匹配值),就可以来到第二个"AB"的位置。


算法时间复杂度为O(m+n)(其中m为字符段长度,n为匹配模式的长度)。


3、算法实践

 

void getNext(const std::string &p, std::vector<int> &next)
{
    next.resize(p.size());
    next[0] = -1;

    int i = 0, j = -1;
    
    while (i != p.size() - 1)
    {
        //这里注意,i==0的时候实际上求的是next[1]的值,以此类推
        if (j == -1 || p[i] == p[j])
        {
            ++i;
            ++j;
            next[i] = j;
        }
        else
        {
            j = next[j];
        }
    }
}

int kmp(const std::string& s, const std::string& p, const int sIndex = 0)
{
    std::vector<int>next(p.size());
    getNext(p, next);//获取next数组,保存到vector中

    int i = sIndex, j = 0;
    while(i != s.length() && j != p.length())
    {
        if (j == -1 || s[i] == p[j])
        {
            ++i;
            ++j;
        }
        else
        {
            j = next[j];
        }
    }

    return j == p.length() ? i - j: -1;
}

 






































以上是关于KMP(字符串匹配算法)的主要内容,如果未能解决你的问题,请参考以下文章

字符串匹配算法——KMP算法

字符串匹配---KMP算法

经典字符串匹配算法——KMP算法

KMP算法及拓展KMP算法

字符串匹配----KMP算法

KMP 字符串匹配算法