数据结构——KMP算法

1.前提介绍

如果有一个串,我们提取其中的部分连续的字符形成的串叫作子串,原来被提取的串叫则叫作主串

我们想要尝试在主串中寻找的子串,叫作模式串,当然这个串不一定找得到,即这个串不一定是主串的子串。

这个寻找过程叫作串的模式匹配

例子:

主串:”hello world!!!“

子串:”ello“

模式串:”er“,”orld“

下面的算法介绍下标从0开始。

2.暴力算法(brute force)

暴力算法又叫串的朴素匹配,听这个名字就知道这个算法有多朴素(baoli)。

暴力算法是我们一开始就能够想起做到的,它就是将主串和模式串作比较,如果这次没有匹配,主串和子串一起回溯,直至匹配成功或失败。

如:

主串s:   ”ababc“

模式串r:”abc“

第一次:a和a匹配        第二次:b和b匹配        第三次:a和c不匹配        第四次:b和a不匹配

第五次:a和a匹配        第六次:b和b匹配        第七次:c和c匹配      

匹配成功。其中第三次和第四次都发生了回溯的现象,尽管第四次不太明显。

我们回头再来看,第三次主串s[2]和子串r[2]不匹配,因此主串退回1下标,子串退回0下标,继续匹配,不匹配则主串回溯这是这个算法带来时间耗费长的原因。

假设主串有n个字符,子串有m个字符。最差情况下,每次都因子串最后一位不匹配,则时间复杂度为O(n*m),例如主串为“aaaaaaaaaaab”,子串为“aaab”。

代码如下:

int brute_force(const string& a, const string& b)
{
    //如果子串比主串长,返回错误
    if (b.size() > a.size())
        return -1;
    for (int i = 0; i < a.size(); i++)
    {
        int j;
        for (j = 0; j < b.size(); j++)
        {
            if (i + j >= a.size() || a[i + j] != b[j])
                break;
        }
        if (j == b.size())
            return i;
    }
    return -1;
}

其实,我们第四次的匹配过程做了无用功,因为第一次匹配和第二次匹配都成功了,而其中子串拿去和主串匹配的a和b是不同的字符,因此当第三次发现不匹配的时候,我们其实完全可以不用第四次的匹配。

利用数学公式来表达就是s[1]=r[1],r[0]≠r[1],则r[0]≠s[1]。

利用这点来跳过第四次,利用一个数组来记录r[0]≠r[1]的信息,这就是KMP算法。

3.KMP算法

KMP名字其实是三个大佬名字的首字母,并没有什么深刻的含义。

KMP算法就是依靠所谓的next数组来实现主串不回溯,而子串回溯的算法,但又因为多求一个子串的next数组,因此时间复杂度只有O(n+m)。

在介绍KMP算法的next数组之前,我们先来介绍前后缀。

(1)最长公共前后缀

一个串的前缀包含第一个字符不包含最后一个字符的子串。

后缀包含最后一个字符不包含第一个字符的子串。

我们求next数组需要的就是最长公共前后缀。

举个例子:

”helleh“       

前缀有”h“,”he“,”hel“,”hell“,”helle“。

后缀有”h“,”eh“,”leh“,”lleh“,”elleh“。

则最长公共前后缀为”h“

”ababab“      同理最长公共前后缀为”abab“,而不是”ababab“。

前后缀的作用就是为了求出下面的next数组。

(2)next数组的定义和手算方法

next数组长度大小和子串长度大小相等,不包含\0符号。而next数组保存的是子串和主串匹配到该位不匹配,子串应该回溯的下标。

但下标为0的元素是个例外,因为假如主串和子串第一个字符就不匹配,这时,应该将主串往后移一位,子串重新匹配,我们将下标为0的元素标为-1,表示这是个特殊情况(其实有更深层的作用,在下面有解释)。

对于next数组作用的公式解释:

 假如有:

主串:”a_{0}a_{1},…a_{t}…,a_{k}a_{k+1}…,a_{n}“ 

模式串:”b_{0},b_{1},b_{2},…b_{t}…,b_{k},b_{k+1}

我们发现有a_{i}=b_{i},i=0,1,2,……,k,但a_{k+1}b_{k+1}

但我们又发现b_{0}=b_{t}b_{1}=b_{t+1},……,b_{t-1}=b_{k}即k+1下标之前的最长公共前后缀为下标0到t-1的元素。

这样,又有b_{0}=a_{t}b_{1}=a_{t+1},……,b_{t-1}=a_{k},我们就可以直接将子串回溯到t,而t就是最长公共前后缀的长度,将b_{t}a_{k+1}继续相比较。

这就是最长前后缀的作用,就是当该字符与主串相应字符不等时,计算到该字符之前的部分的最长前后缀的长度t,然后把子串移到t下标继续和主串比较。

举个例子:

”abababh“

next第一个元素为-1

next第二个元素为0,”a“最长公共前后缀长度为0。

next第三个元素为0,”ab“最长公共前后缀长度为0。

next第四个元素为1,”aba“最长公共前后缀长度为1。

next第五个元素2,”abab“最长公共前后缀长度为2。

等等……,最后求得的next数组为-1 0 0 1 2 3 4。

这样,next数组一旦求出就可以用来匹配任意一个主串。

我们如何用代码取得这样的一个数组呢?我们利用动态规划的思想来求出。

已知next[0]=-1,假设我们用i表示遍历子串字符的变量,再用j来表示当前字符next数组的值。用subString来表示子串。因此,j一开始为-1,i一开始为0。

至于为什么next[0]=-1,是因为我们在循环里设定了一个if,当j=-1时,则i++,j++这样一开始就可以比较next[i]和next[j]。

注意j的意义,j既是子串该字符之前部分的最长前后缀的长度,也是当前字符和主串字符匹配失败,子串应当回退的下标,则有subString[0],subString[1],……,subString[j-1]和subString[j],subString[j+1],……,subString[i-1]对应相等,因此subString[i] == subString[j]时,next[i+1]=next[j+1]。

且当subString[i] ≠ subString[j]时,则我们需要求出subString[0],subString[1],……,subString[j-1],subString[j],……,subString[i]的最长前后缀,我们知道这时已经没有比j更长的最长前后缀,我们开始将j=next[j],往后回溯,即等价于求subString[0],subString[1],……,subString[j-1]的最长前后缀,这时i没有变化,以待下一次的比较。

因此分为下面三种情况:

1.j=-1时,说明这时子串之前的字符也和主串当前字符没有能够匹配的,因此主串要后移一位,子串重新回退到0,重新匹配。

2.subString[i] == subString[j]时,则next[i+1]=next[j+1]。

3.subString[i] ≠subString[j]且j≠-1时,j=next[j],待下一次循环重复这三种情况。

void get_next(const string& subString, int* next)
{
    int i = 0, j = -1;
    next[0] = -1;
    while (i < subString.size() - 1)
    {
        if ((j == -1) || (subString[i] == subString[j]))
        {
            i++;
            j++;
            next[i] = j;
        }
        else j = next[j];
    }
    for (int i = 0; i < 4; i++)
        cout << next[i] << " ";
    cout << endl;
}

(3)KMP算法解释

假如说我们要匹配"cabaabababh",我们字串匹配到这步:

 发现子串b和主串a不相等,我们将子串回溯到next[不匹配b的下标]即1位置,变成下面:

很神奇的是,我们发现主串并没有回溯,而字串已经有一个a与主串匹配。 

这样一来,KMP的匹配方法有三种情况:

第一种情况:a[i]==a[j],则i++,j++。

第二种情况:a[i]!=a[j],且j=0,则j=next[0]=-1,等待下一次进入循环时i++,j++这样主串就往后移一位,而子串重新匹配。

第三种情况:a[i]!=a[j],则j=next[j]

(4)KMP存在问题

主串:”gooloegoogle“

模式串:”google“

next数组为-1 0 0 0 1 0

到这一步:

我们发现子串g和l不匹配,于是子串回溯到0,发现子串还是g和主串l不匹配,这也是无用功。

因此,我们在求next数组时加上if(subString[i]!=subString[j]) next[i]=j;else next[i]=next[j];这样,就不会再出现这样的情况。

求next数组的代码: 

void get_next(const string& subString, int* next)
{
    int i = 0, j = -1;
    next[0] = -1;
    while (i < subString.size() - 1)
    {
        if ((j == -1) || (subString[i] == subString[j]))
        {
            i++;
            j++; 
            if(subString[i]!=subString[j])
                next[i]=j;
            else next[i]=next[j];
        }
        else j = next[j];
    }
}
int KMP(const string& mainString, const string& subString)
{
    if (subString.size() > mainString.size())
        return -1;
    int i = 0, j = 0;
    int* next = new int[b.size()];
    get_next(b, next);
    while (i < mainString.size() && j < subString.size())
    {
        if (j == -1 || a[i] == b[j])
            i++, j++;
        else j = next[j];
    }
    if (j >= b.size())
        return i - b.size();
    else return -1;
}
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值