一线大厂——KMP算法

一、引言,何为KMP算法


        KMP算法是一种改进的字符串匹配算法,由D.E.Knuth,J.H.Morris和V.R.Pratt提出的,因此人们称它为克努特—莫里斯—普拉特操作(简称KMP算法)。KMP算法的核心是利用匹配失败后的信息,尽量减少模式串与主串的匹配次数以达到快速匹配的目的。具体实现就是通过一个next()函数实现,函数本身包含了模式串的局部匹配信息。KMP算法的时间复杂度O(m+n)

下面开始介绍KMP算法,在此先规定好文章中所用变量代表意义:
字符串A为被查找对象,字符串B为查找对象,即在A中寻找B是否存在
数组 next 为字符串B经过KMP算法之后得到的用于记录指针移动的数组
设字符串A的长度为m,字符串B的长度为n
用#代表一个为止的字符
k为头字符串指针,j为尾字符串指针

二、暴力算法


遇到字符串匹配问题(在字符串A中查找字符串B是否存在),很多初学者第一时间想到的就是遍历整个字符串A。当遇到A、B首字母相同的时候开始遍历字符串B判断是否相同,若不相同则字符串B向后移动,直到遇到下一次A、B首字母相同再次开始遍历,最后发现相同的时候退出。

乍一看,对于上图两个字符串A、B,似乎暴力算法也不算很麻烦,但是请看下图的两个字符串

对于字符串B,每次需要遍历完整个字符串才会发现最后一个不相同,于是字符串B向后挪动一位继续重新开始比较,于是可以得出整个算法时间复杂度大概为O(m * n),当m、n非常大时间可以看出我们浪费了很多不必要的时间

三、过渡(思路)


不难发现对于图2中的两个字符串,浪费我们时间的主要因素有两点:
1.对于A中的每一个字母都可以和B的首字母匹配,于是每个A中的字符都可以作为B的起点,总共     需要比较m次。
2.对于B中的首字母之后的字母恰好可以和A中充当首字母的之后的字母匹配上。
因素一对应时间负责度O(m* n)中的m,而因素二则对应n

于是得到这样一个思路,既然字符串B前部分aaa相同,只有最后的b是不匹配的,那么我们是不是可以把字符串B向后移动一个字符,然后从第三个字符开始比较呢?即字符串A中全部是a,我们只需要在其中找到一个字符b即可判断字符串B存在,如下图:

 这样一来便不再需要比较 m * n 次,我们只需要判断字符串B向后移动一位之后,A中的第四个字符能否和B中的第三个字符匹配。若能,则将A之前的第四位字符看做当前的第三位字符,向后寻找新的第四位字符。
更通俗的解释就是:
当前字符串A是 aaa#1#2,已知#1不是b,那么若#1是a,则字符串A变成功aaaa#2,此时去掉第一个a,即(a)aaa#2,现在只需要判断#2是不是b,若是则B存在,若不是则重复上述步骤

于是整体思路已确定,接下来的问题是如何判断数组B的指针在不匹配之后应该指向哪里,这个过程只需要字符串B,即每个字符串B有且仅有唯一对应的数组next。
先继续从图3的字符串B入手,得出数组B的核心在于,判断最长相同头字符串、尾字符串,过程如下:

当指针指向第三个字符的时候判断第二个字符前面的部分“a1a2”,最长相同头字符串、尾字符串是“a1”和“a2”,即当第三个字符不匹配的时候,返回到第一个字符之后(为什么没有判断前两个字符的原因稍后会解释)

 当指针指向第四个字符的时候判断第二个字符前面的部分“a1a2a3”,最长相同头字符串、尾字符串是“a1a2”和“a2a3”,即当第三个字符不匹配的时候,返回到第二个字符之后

为了实现上述过程,我们需要两个指针,一个为最长相同头字符串,一个为最长相同尾字符串,当两个指针指向的字符相同的时候同时向后移动,并记录下之前相同的长度,整体过程如下

值得注意的是,每次判断的过程是,先判断->再移动->移动之后记录,而不是先移动->再判断->再记录,对于上述next数组的意义即,判断B指向的字符和A指向的字符不同的时候,B的指针,返回到对应next记录的下标处。如最后一个字符b不同,那么B的指针指向B[next[3]],即B[2],即a3
而考虑到不是所有B都是k,j都是同时移动的,可能出现k,j指向字符不相等情况,这个时候只有一个指针可以移动,于是考虑到先判断再移动的顺序,将next[0]设为-1,k,j不匹配的时候k = next[k],如此可以防止头字符串中也存在相同头字符串和尾字符串,如图6

以下列举一个k会左移的字符串B求next数组的过程:

 

四、具体代码实现


整体思路过程已经确定好了,接下来就是代码的实现,在此以函数的形式写出对next数组赋值的操作。

void GetNext(char* t, int* next)      //t为字符串B,next为创建的数组next
{
    int j, k, str = strlen(t);        //k为头字符串指针,j为尾字符串指针
    j = 0; k = -1;                    //首先初始化K为-1,因为判断过程先先判断在移动
    next[0] = -1;                     //next[0]为-1是为了之后头字符串的移动
    while (j <  str - 1)
    {
        if (k == -1 || t[j] == t[k])  //先判断
        {
            j++; k++;                 //再移动
            next[j] = k;              //再记录,k指针指向的位置就是AB字符不匹配的时候B指针应当返                    
                                      //回的位置
        }
        else
        {
            k = next[k];              //这一步很关键,表明了不匹配的时候k指针应当回到的位置
                                      //具体可以参考图5和图6
        }
    }
}

 在此附上力扣题目的链接,感兴趣可以去尝试以KMP写法提交:https://leetcode-cn.com/problems/implement-strstr/

KMP算法是一种字符串匹配算法,用于在一个文本串S内查找一个模式串P的出现位置。它的时间复杂度为O(n+m),其中n为文本串的长度,m为模式串的长度。 KMP算法的核心思想是利用已知信息来避免不必要的字符比较。具体来说,它维护一个next数组,其中next[i]表示当第i个字符匹配失败时,下一次匹配应该从模式串的第next[i]个字符开始。 我们可以通过一个简单的例子来理解KMP算法的思想。假设文本串为S="ababababca",模式串为P="abababca",我们想要在S中查找P的出现位置。 首先,我们可以将P的每个前缀和后缀进行比较,得到next数组: | i | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | | --- | - | - | - | - | - | - | - | - | | P | a | b | a | b | a | b | c | a | | next| 0 | 0 | 1 | 2 | 3 | 4 | 0 | 1 | 接下来,我们从S的第一个字符开始匹配P。当S的第七个字符和P的第七个字符匹配失败时,我们可以利用next[6]=4,将P向右移动4个字符,使得P的第五个字符与S的第七个字符对齐。此时,我们可以发现P的前五个字符和S的前五个字符已经匹配成功了。因此,我们可以继续从S的第六个字符开始匹配P。 当S的第十个字符和P的第八个字符匹配失败时,我们可以利用next[7]=1,将P向右移动一个字符,使得P的第一个字符和S的第十个字符对齐。此时,我们可以发现P的前一个字符和S的第十个字符已经匹配成功了。因此,我们可以继续从S的第十一个字符开始匹配P。 最终,我们可以发现P出现在S的第二个位置。 下面是KMP算法的C++代码实现:
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值