1. KMP算法整理总结----附代码

KMP算法

用于解决字符串的模式匹配问题。它是求一个字符串(模式串)在另一个字符串(主串)中的位置。

首先最简单的一个方法就是暴力解法,通过一个循环,依次从主串中来匹配模式串。假设n、m分别表示主串和模式串的长度,那么按照暴力解法而言,最坏情况下的时间复杂度就是 O ( n ∗ m ) O(n*m) O(nm). KMP算法能够将这个暴力解法的时间复杂度降到线性的数量级。
假设目前我们有一个主串T和一个模式串P,如下所示:
image
主串和模式串分别有一个指针指向,分别是i和j:
image
按照暴力解法而言,接下来只需要同时移动i和j,然后比较其指向的字符是否一致即可。直到出现不一致的字符;
image
当出现不一样的字符之后,j则回退到模式串的第0位(假设下标是从0开始的),i则回退到主串的第1位,然后继续执行之前的过程。
image
假设n、m分别表示主串和模式串的长度,那么按照暴力解法而言,最坏情况下的时间复杂度就是 O ( n ∗ m ) O(n*m) O(nm).最坏的情况比如主串是“AAAAAB”,模式串是“AAB”.

KMP算法的出发点是:尽可能的利用已经部分匹配好的有效信息,保持i指针不进行回溯,通过修改j指针,让模式串尽可能的移动到有效位置,从而进行后续比对. 因此这就带来第一个问题,j指针的位置要如何计算呢?

下面我们先探讨一下j指针的移动规律:
image
如上图,此时i和j所对应的字符不匹配,那么j指针最好应该移动到哪个位置呢?之前的暴力解法需要i的回溯,因此j指针直接回退到0位置,但是我们为了更多的利用已经匹配好的信息,所以希望其能够回退到j=1的位置与i的位置对应,进行下一轮比对。为什么呢?因为此时有一个相同的子串A在i-1的位置和j=0的位置上对应了。
image
如下图也是同样的情况:
image
在这种情况下,我们最希望j回退到j=2的位置,然后与此时的i位置进行比对,这是因为此时前面有AB一一相同对应。
image

到此为止,我们仿佛已经找到了当匹配失败时,j指针要移动到的下一个位置k存在着这样的性质:P[0,k-1]=P[j-k,j-1].这个j表示的是发生匹配失败时模式串上对应的匹配失败的位置编号。 也就是上上图红色框的j的位置。可以通过下面这个图加以理解。
image

其实上面这个过程就是在找模式串的最长公共前后缀。其定义是:假设有一个串"p0 p1 p2 p3…pj",如果存在一个情况是“p0 p1 … pk = p_j-k … p_j”,那么我们就说这个串中有一个最大长度是k+1的最长公共前后缀。这里之所以要加一是因为下标是从0开始的,具体情况具体分析。
这就带来一个问题,如何来寻找前后缀呢?在找前缀时,要找除了最后一个字符的所有子串;在找后缀时,要找除了第一个字符的所有子串。
我们举个栗子:
以串abaabca为例,其对应的各个子串的最长公共前后缀统计如下所示:
image
此时最长公共前后缀长度就会和模式串P的每个字符产生一个对应关系,如下:
image
上述这个表的含义是,当以某个字符作为最后一个字符时,当前子串所拥有的最长公共前后缀的长度。

接下来引入next数组,next数组记录了模式串中每个字符位置上发生匹配失败时,下一次应该和主串对应位置进行比对的字符的下标。如模式串中在j 处的字符跟主串在i 处的字符匹配失配时,下一步用next [j] 处的字符继续跟主串i 处的字符匹配,相当于模式串向右移动 j - next[j] 位。

可参考Blog:手动推导next数组

使用编程思想来递推求解next数组:这里要明确一点的是,next数组仅仅是针对模式串进行分析。
我们设next[0]=-1,为什么这样做呢?

这实际上对应着一种情况:当j为0的时候,此时就发生不匹配。如下图所示:

image
由于在第一个位置上就发生了匹配失误的问题,此时j是无法再向左移动,因此需要将主串i向右移动一位。这就是为什么将next[0]初始化为-1。

第二种情况:在第二个位置上发生匹配失误的问题,此时模式串左边只有一个位置A,此时j只能向左移动一个,因此next[1]=0。(解释一下next[j]的含义就是,当进行字符匹配的过程中,在模式串的第j的位置发生了匹配失误时,下次匹配时应该与主串i所对应的字符所在的下标。)
image

第三种情况,就是当pk=pj的时候,我们可以得出这样一个结论:next[j+1]=next[j]+1=k+1
image

第四种情况,更为普遍的一种情况,比如pk != pj时
image
此时k=next[k],为什么会有这样的结论呢?本人是这么理解的。以上图为例,假设在j的位置上发生了匹配失误,那么按照之前的说法,此时应该是next[j]=k,但是目前我们还有一个信息,那就是我们已经直到pk != pj,而kmp的思想就是尽可能的利用已知的匹配信息,所以如果依然按照之前的做法继续进行匹配的话,这一步是有些多余的。因此我们需要再找一个更短的公共前后缀,就像是下图所示的这个样子,此时就可以继续顺利的进行匹配了,同时此步操作显得并不多余。
image

KMP算法的时间复杂度为O(n+m),n是主串的长度,m是模式串的长度;
至此,next数组的建立过程就已经都解释清楚了。附相关代码:
暴力解法

int BF(SString T[],SString P[])
/**
 * @description: 暴力匹配(朴素模式匹配BF)
 * @param : 
 * @return {int} 
 */
{
    
    int i=0,j=0;

    while (i < strlen(T) && j < strlen(P))
    {
        if (T[i] == P[j])
        {
            i++;
            j++;

        }else
        {   
            i=i-j+1;
            j=0;

        }        
    }
    if (j >= strlen(P))
    {
        printf("匹配成功.\n");
        printf("模式串在主串中的编号地址为:%d\n",(i-strlen(P)));
        return i-strlen(P);

    }else
    {
        printf("匹配失败.\n");
        return 0;
    }
}

计算next数组

void GetNext(SString P[],int next[])
{   

    int j = 0, k = -1;
    next[j] = k;
    while (j < strlen(P))
    {
        if(k==-1 || P[k]==P[j])
        {
            j++;
            k++;
            next[j]=k;
        }else{
            k = next[k];
        }
    
    }
}

KMP算法:这里 特别要注意算法中的while循环的条件 ,这里添加了j==-1这一条件。这是因为我们将next[0]设置为-1,如果不添加这个条件的话,那么在访问数组的过程中,就会访问到数组的-1处的位置,从而会导致算法提前结束,因此需要添加这一条件。当然,具体情况还需要具体分析。

int KMP(int start,char T[],char P[],int next[])
{
	int i=start,j=0;

	while(j == -1 || (i < strlen(T) && j < strlen(P)))
	{
		if(j==-1||T[i]==P[j])
		{
			i++;         //继续对下一个字符比较 
			j++;         //模式串向右滑动 
		}
		else{
            j=next[j];
        } 
        // printf("i的变化:%d\n",i);
        // printf("j的变化:%d\n",j);
	}

	if(j >= strlen(P)){
        printf("匹配成功.\n");
        printf("模式串在主串中的编号地址为:%d\n",(i-j));
        return (i-j);    //匹配成功返回下标 
    }
	else{
        printf("匹配失败.\n");
        return -1;                 //匹配失败返回-1 
    } 
}

主函数部分

#include <windows.h>
#include <iostream>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "time.h"

using namespace std;

typedef char SString;

int main()
{

    SString T[]="pktsinghusttjpuhit";
    SString P[]="tsing";

    // int index1 = BF(T,P);    //暴力解法

    int next[strlen(P)];    //定义next数组
    GetNext(P,next);    //计算next数组

    // cout<<"计算得到的next数组元素为:"<<endl;
    // for(int i = 0;i < strlen(P);i++){
    //     cout << next[i] <<" ";
    // }

    int index2 = KMP(0,T,P,next);   //KMP方法

}

通过实际比较,发现越复杂的情况下KMP算法的时间优势越是明显,当然,有些简单的情况下,由于KMP算法还需要维护一个next数组,此时KMP算法的时间复杂度要比暴力解法的时间复杂度高。

该blog所参考的图片来源:
Blog1
Blog2
Blog3

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值