【数据结构】串模式匹配及KMP算法详解——看不懂来砍我

本篇博客参考了以下几篇文章和视频:
参考1 参考2 武大MOOC
初学数据结构,第一次遇到了一根难啃的骨头——串的模式匹配算法(KMP),虽然网上关于KMP算法的介绍很多,但多半叙述不全面,推导不流畅。因此自己写了这篇博客,尽可能以因果关系呈现KMP算法。
本篇博客尽量以最通俗,详尽的语言解释KMP算法,全文干货,无废话。

KMP是用来干嘛的?

简而言之,KMP算法较好的解决了串的模式匹配问题,即判断模式串(t)是否是目标串(s)的子串,注意子串包含目标串本身,也包含空串。例如aaab是aaabaab的子串,abc是abc的子串。

为什么非要用KMP?

简而言之,因为KMP算法具有很好的效率。一般来讲模式匹配算法有两种即Brute-Force(BF)暴力算法与KMP算法

那为什么不用BF算法?

简而言之,因为BF算法非常的低效。BF算法采用穷举思想,这也是绝大多数人解决模式匹配问题最先想到的算法。
本文皆用下面的模式串(t)和目标串(s) 举例
s = “abaabaabbabaaabaabbabaab”
t = "abaabbabaab"

蓝色表示匹配成功,红色表示匹配失败。
BF算法最大的问题就在于每当匹配失败时,模式串都要后移一位,然后再从模式串的头部重新与目标串匹配(术语:回溯)。比如下图的step.1-5,step.10-19匹配都没有问题,但在step.6和step.20出现了匹配失败,导致的结果就是模式串后移一位,从头再来。这实质上是一种很无脑、很浪费、很低效的一种算法。
为什么说它浪费?因为它忽视了一些关键信息
拿step.6来说,匹配失败,在step.7模式串后退一位,其结果必然也是失败的,因为在step1-6的过程中算法已经遍历过s[1]了,s[1]=b而不是a,紧接着在step.9匹配也失败了,其结果其实必然也是失败的,因为在step1-6的过程中算法已经遍历过s[3]了,s[3]=a而不等于t[1]即b
简单的来说,在step.6匹配失败后,模式串t是可以直接移位到step.10的位置的!因为在step.1-6就已经验证了,模式串的头部a在aba(目标串前3位)里是不可能匹配成功的,直接跳过就行,这就是关键信息。
同理step.20匹配失败后,模式串的头部可以直接移位到abaabaabb a baaabaabbabaabb,加粗斜体的a的位置。

在这里插入图片描述
这里就不贴上BF算法的代码了。

BF算法分析

1.算法在字符比较不相等,需要回溯即i = i - j + 1:即回退到s中的下一个字符开始进行字符匹配。
2.最好情况下的时间复杂度为O(m+n)
3.最坏情况下的时间复杂度为O(m*n)
m,n分别为串t,s的长度。
在这里插入图片描述

一些概念及定义

前缀的概念:指的是字符串的子串中从原串最前面开始的子串,如abcdef的前缀有:a,ab,abc,abcd,abcde
后缀的概念:指的是字符串的子串中在原串结尾处结尾的子串,如abcdef的后缀有:f,ef,def,cdef,bcdef
注意前缀与后缀不包括原串本身。
最大公约缀的概念:这个概念是我自己起的…对应最大公约数,也就是某个串,既在前缀中又在后缀中,并且它是最长的。

规律?

以上的关键信息及模式串的移位由分析得出,那么怎么不分析,直接得出下次移位的下标位置呢
先观察规律。
在这里插入图片描述

紫框是模式串与目标串在step.1-6里确定的最长的匹配串段abaab,很容易可以看出,abaab的最大公约缀是ab,上文分析得到:模式串t是可以直接移位到step.10的位置的!因为在step.1-6就已经验证了,模式串的头部a在aba(目标串前3位)里是不可能匹配成功的,直接跳过就行,这就是关键信息。 有意思的是,step.10中,模式串头部a所在位置刚好就是目标串最长匹配串段abaab(加粗) 中a的位置,后缀的第一个字母a!
再看黄框,abaabbabaa是模式串与目标串在step10-20所确定的最长匹配串段,上文分析得到:模式串的头部可以直接移位到abaabaabb a baaabaabbabaab,加粗斜体的a的位置。abaabbabaa的最大公约缀是abaa,而abaabaabb a baaabaabbabaab 这个a的位置刚好就是step.19配对时最长匹配串段的后缀的第一个字母a。
在这里插入图片描述

!!!同时注意到跳转之后,模式串和目标串必定会有部分元素相同(即最大公约缀),因此在KMP算法中,跳转之后,直接从最大公约缀后一位开始比较,即棕色箭头。!!!
既然理解了如何跳到下次匹配位置,我们的KMP算法就应运而生了。
即根据不同长度的最长匹配字段->算出最大公约缀->若匹配失败,根据最大公约缀直接跳转到后缀处的第一个字母!
这样就避免了BF算法回溯带来的低效与浪费

KMP实战

怎么求最大公约缀?

事实上,跳转是依靠数组下标完成的,因此我们需要知道不同长度最大匹配串段的最大公约缀的长度(量化)。一般的,我们用next数组来存储这个长度
仍然用这个例子。
s = "abaabaabbabaaabaabbabaab"每个元素用s[i]表示
t = "abaabbabaab"每个元素用t[j]表示

j012345678910
t[j]abaabbabaab
next[j]-10011201234
最大公约缀aaabaababaabaa

几个注意点:
1.长度大于2的模式串,next[0]=-1,next[1]=0
2.next[j]=-1说明模式串t[j]之前没有任何用于加速匹配的信息,下一趟应从t的开头j++(j=0)开始匹配
3.t[j]前的最大匹配串段在计算最大公约缀的时候,不包括t[j]本身

这样,我们就得到了next数组。

怎么理解next数组?

主要是理解这句话:next[j]=k说明模式串t[j]之前有k个字符已成功匹配,下一趟应从t[k]开始匹配。
在step.6时,发生了匹配失败,此时i = j = 5,查上表,next[5] = 2,说明模式串t[5]之前有2个字符已经成功匹配,下一趟应从t[5]开始匹配。
嗯哼,这不就是上文 !!!同时注意到跳转之后,模式串和目标串必定会有部分元素相同(即最大公约缀),因此在KMP算法中,跳转之后,直接从最大公约缀后一位开始比较,即棕色箭头。!!! 的意思吗。
!!!具体分析一下下图,i = j = 4时,即棕色箭头,匹配成功。i = j = 5时,即紫色箭头,匹配失败,查表,next[5] = 2,那么j跳转到j = 2,继续比较(此时i = 3,i = 4与j = 0,j = 1必然是匹配的)。
同理,i = j = 10时,即淡紫色箭头,匹配失败,查表,next[10] = 4,那么j跳转到j = 4,继续比较(此时i = 6.7,8,9必然与j = 0,1,2,3匹配)。!!!

在这里插入图片描述
至此,把以上的过程全部连贯起来,就得到下面这个动图。图源侵删

是不是感觉豁然开朗呢,如果还不懂KMP,请逐字逐句,思考着再看几遍。如果看完仍然不懂,请来砍我。

KMP算法实现

一些基本定义

#include <iostream>
using namespace std;
#define MAXSIZE 100
typedef struct 
{
    char data[MAXSIZE];
    int length;//字符串长度
}SqString;

求next数组算法实现

//求模式串t的next值的算法
void GetNext(SqString t, int next[])
{
    int j = 0, k = -1;
    next[0] = -1;
    while (j < t.length - 1)
    {
        if (k == -1 || t.data[j] == t.data[k])
        {
            j++;
            k++;
            next[j] = k;
        }
        else
        {
            k = next[k];//这是非常神奇的一句
        }
    }
}

建议用笔走一下上面的代码,手动求下next。

KMP算法实现

//KMP
int KMPIndex(SqString t, SqString s)
{
    int next[MAXSIZE], i = 0, j = 0;
    GetNext(t, next);
    while (i < s.length && j < t.length)
    {
        if (j ==  -1 || s.data[i] == t.data[j])
        {
            i++;
            j++;
        }
        else
    {
        j = next[j];//i不变,j后退
    }
    }
    if (j >= t.length)
    {
        return i - t.length;//返回匹配模式串的首字符下标
    }
    else
    {
        return -1;//返回不匹配
    }
}

同上建议手动走一下。
这两段代码很值得咀嚼。

KMP算法分析

1.在KMP算法中求next数组的时间复杂度为O(m),在后面的匹配中因主串s的下标不减即不回溯,比较次数可记为n,所以KMP算法平均时间复杂度O(m+n)
m,n分别为串t,s的长度。

KMP测试

自己写的测试程序,复制粘贴即可运行:

#include <iostream>
using namespace std;
#define MAXSIZE 100
typedef struct
{
    char data[MAXSIZE];
    int length;//字符串长度
}SqString;

//求模式串t的next值的算法
void GetNext(SqString t, int next[])
{
    int j = 0, k = -1;
    next[0] = -1;
    while (j < t.length - 1)
    {
        if (k == -1 || t.data[j] == t.data[k])
        {
            j++;
            k++;
            next[j] = k;
        }
        else
        {
            k = next[k];
        }
    }
    cout << "next数组为:";
    for (int i = 0; i < t.length; i++)
    {
        cout << next[i];
    }
    cout << "\n";
}

//KMP
int KMPIndex(SqString t, SqString s)
{
    int next[MAXSIZE], i = 0, j = 0;
    GetNext(t, next);
    while (i < s.length && j < t.length)
    {
        if (j == -1 || s.data[i] == t.data[j])
        {
            i++;
            j++;
        }
        else
        {
            j = next[j];//i不变,j后退
        }
    }
    if (j >= t.length)
    {
        return i - t.length;//返回匹配模式串的首字符下标
    }
    else
    {
        return -1;//返回不匹配
    }
}
int main()
{
    SqString t;
    cout << "请输入t.length:";
    cin >> t.length;
    cout << "请输入t.data:";
    for (int i = 0; i < t.length; i++)
    {
        cin >> t.data[i];
    }
    SqString s;
    cout << "请输入s.length:";
    cin >> s.length;
    cout << "请输入s.data:";
    for (int i = 0; i < s.length; i++)
    {
        cin >> s.data[i];
    }
    cout << "KMP返回值为:"<< KMPIndex(t, s);
}

一些测试结果:

匹配成功返回串首字符下标
在这里插入图片描述
不匹配返回-1
在这里插入图片描述
匹配成功返回串首字符下标
在这里插入图片描述

KMP改进

KMP也是有瑕疵的,可以进一步改进,大概思路是将next数组改为nextval数组,有兴趣请点击下方链接。
12分17秒开始

以上 如果此篇博客对您有帮助欢迎点赞与转发 有疑问请留言或私信 2020/8/3

  • 15
    点赞
  • 56
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值