带你轻松学会 KMP 算法


简介

  KMP算法是一种字符串模式匹配算法, 不同的来源讲解方式也不一样,很容易混乱,在这里以一种特殊的方式来讲解KMP算法,希望大家不再被这个问题所困扰。

  KMP 算法是 D.E.Knuth、J,H,Morris 和 V.R.Pratt 三位神人共同提出的,称之为 Knuth-Morria-Pratt 算法,简称 KMP 算法。该算法相对于 Brute-Force(暴力)算法有比较大的改进,主要是消除了主串指针的回溯, 从而使算法效率有了某种程度的提高。

什么是字符串的模式匹配?

  给定两个串S=“s1s2s3 …sn”和T=“t1t2t3 …tn”,在主串S中寻找子串T的过程叫做模式匹配,T称为模式。

我们先从比较好理解的 暴力匹配朴素模式匹配BF算法)开始,进而引出 KMP算法。

暴力匹配(朴素模式匹配BF)

规定i是主串S的下标,j是模式T的下标。现在假设现在主串S匹配到 i 位置,模式串T匹配到 j 位置。

  • 如果当前字符匹配成功(即S[i] = T[j]),则i++; j++;,继续匹配下一个字符;
  • 如果失配(即 S[i] != T[j] ),令i = i-(j-1),j = 0;。相当于每次匹配失败时,i 回溯到本次失配起始字符的下一个字符,j 回溯到0。
int BF(char S[],char T[])
{
	int i=0,j=0;
	while(S[i] != '\0' && T[j] != '\0')
	{
		if(S[i] == T[j])
		{
			i++;
			j++;
		}
		else
		{
			i=i-j+1;
			j=0;
		}
	}
	if (T[j]=='\0') return (i-j);     //主串中存在该模式返回下标号 
	else return -1;     //主串中不存在该模式 
}

举个例子:

现在有主串 S:ababcabcacbab,模式串T:abcac。我们来看一下是如何匹配的。i从0开始,j也从0开始。

  • 第一次匹配中,i从0开始,j从0开始。当i=2,j=2时匹配失败,此时i回溯到1,j回溯到0。

在这里插入图片描述

  • 第二次匹配中,i从1开始,j从0开始。当i=1,j=0时匹配失败,此时i回溯到2,j回溯到0。

在这里插入图片描述

  • 第三次匹配中,i从2开始,j从0开始。当i=6,j=4时匹配失败,此时i回溯到3,j回溯到0。

在这里插入图片描述

  • 第四次匹配中,i从3开始,j从0开始。当i=3,j=0时匹配失败,此时i回溯到4,j回溯到0。

在这里插入图片描述

  • 第五次匹配中,i从4开始,j从0开始。当i=4,j=0时匹配失败,此时i回溯到5,j回溯到0。

在这里插入图片描述

  • 第六次匹配中,i从5开始,j从0开始。i=10,j=5,T中全部字符比较完,匹配成功,返回本次匹配起始位置下标i - j。(i=9和j=4的时候匹配成功,i和j会再加一次,所以i=10,j=5)

在这里插入图片描述

  可见,如果 i 已经匹配了一段字符后出现了失配的情况,i 会重新往回回溯,j 又从0开始比较。这样浪费的大量的时间。

  • 在第三次匹配结束后,我们可以发现:i=3和j=0,i=4和j=0以及i=5和j=0是不必进行的!

  • 因为从第三次部分匹配过程中我们可以得出,主串中第3,4,5个字符必然是‘b’,‘c’,‘a’(即与模式串的第1,2,3个字符分别对应相等),而模式的首字符是‘a’,它分别与‘b’,‘c’不等,与‘a’相等。

如果将模式向右滑动3个字符继续进行i=6和j=1时的字符比较,很明显会加快进程。这样就引出了我们的KMP算法,不回溯i,加快匹配效率。

KMP 算法

  上面说道 KMP 算法主要是通过消除主串指针的回溯来提高匹配的效率的,那么,它是怎么样来消除回溯的呢? 提取并运用了加速匹配的信息!

算法流程:

规定i是主串S的下标,j是模式T的下标。现在假设现在主串S匹配到 i 位置,模式串T匹配到 j 位置。

  • 如果j = -1,则i++,j++,继续匹配下一个字符;
  • 如果S[i] = T[j],则i++,j++,继续匹配下一个字符;
  • 如果j != -1,且S[i] != P[j],则 i 不变,j = next[j]。此举意味着失配时,接下来模式串T要相对于主串S向右移动j - next [j] 位。

  加速信息,即数组 next 的提取是整个 KMP 算法中最核心的部分,弄懂了 next 的求解方法,也就弄懂了 KMP 算法的十之七八了,但是不巧的是这部分代码恰恰是最不容易弄懂的……

KMP算法代码:

int KMP(int start,char S[],char T[])
{
	int i = start, j = 0;
	while(S[i]!='\0' && T[j]!='\0')
	{
		if(j==-1 || S[i]==T[j])
		{
			i++;	//继续对下一个字符比较 
			j++;    //模式串向右滑动 
		}
		else j = next[j];	// 此语句是这段代码最反人类的地方
	}
	if (T[j] == '\0') return (i-j);    // 匹配成功返回下标 
	else return -1;                	   // 匹配失败返回-1 
}

到这里,我们肯定是有一些疑问的:

  1. next是什么???它是怎么来的???

首先我们来解释一个名词:最长公共前后缀。假设有一个串P=“p0p1p2 …pj-1pj”。如果存在p0p1…pk-1pk = pj-kpj-k+1…pj-1pj,我们就说在P串中有一个最大长度为k+1的公共前后缀。

  1. 如何寻找前后缀???
  • 找前缀时,要找除了最后一个字符的所有子串。
  • 找后缀时,要找除了第一个字符的所有子串。

问题1、2实例:

现在有串P=abaabca,各个子串的最大公共前后缀长度如下表所示:

在这里插入图片描述

这样,公共前后缀最长长度就会和串P的每个字符产生一种对应关系:

在这里插入图片描述

  这个表的含义是以当前子串的第一个字符、最后一个字符列出来的后缀、前缀,包括当前子串所拥有的公共前后缀最长长度。例如当c作为最后一个字符时,当前子串abaabc并没有公共前后缀。

  接下来我们就用这个表来引出next数组,next 数组的值是除当前字符外(注意不包括当前字符)的公共前后缀最长长度,相当于把上表做一个变形,将表中公共前后缀最长长度全部右移一位,第一个值赋为-1。

  例如c对应next值的意义是,c之前(不包括c)的子串abaab所拥有的公共前后缀最长长度为2,我们称next数组中的值为失效函数值,也就是c的失效函数值为2。(当然这是我们手动推得,我们后续会用编程思想来推得next数组)

在这里插入图片描述

我们手动推得了next数组,那就来体验一下KMP算法的流程:现在有主串S:ababcabcacbab,模式串T:abcac。i从0开始,j也从0开始。

根据上述方法可以知道,T的中每个字符对应的失效函数值为:
在这里插入图片描述

  • 第一次匹配中,i从0开始,j从0开始。当i=2,j=2时匹配失败,此时i不动,next[j] = next[2] = 0,接下来模式串T要相对于主串S向右移动j - next [j] = 2 位,j回溯到0。

在这里插入图片描述

第二次匹配中,i从2开始,j从0开始。当i=6,j=4时匹配失败,此时i不动,next[j]=next[4]=1,接下来模式串T要相对于主串S向右移动j - next [j] = 3位,j回溯到1。

在这里插入图片描述

第三次匹配中,i从6开始,j从1开始。当i=10,j=5时匹配成功,返回i - j = 5。

哼哼

我们根据next数组进行匹配,不失一般性的话,我们做如下总结:

当 pj 和 si 失配时,因为next[j]=k,相当于在不包含pj的模式串中有最大长度为k的相同前后缀,也就是 p0 p1 …pk-1 = pj-k pj-k+1 … pj-1,所以令j = next[j],让模式串右移j - next[j]位,使模式串 p0 p1 … pk-1 与主串 si-k si-k+1 … si-1 对齐,让 pk 和 si 继续匹配。

代码推导 next 数组:

void Getnext(int next[],string t)
{
    int j = 0, k = -1;
    next[0] = -1;
    while(j < t.length()-1)
    {
        if(k == -1 || t[j] == t[k]){
            j++, k++;
            next[j] = k;
        }
        else k = next[k];
    }
}

KMP的next数组告诉我们:当模式串中的某个字符跟主串中的某个字符失配时,模式串下一步应该跳到哪个位置。如模式串中在j 处的字符跟主串在i 处的字符匹配失配时,下一步用 next [j] 处的字符继续跟主串 i 处的字符匹配,相当于模式串向右移动 j - next[j] 位。

测试代码

int main(){
    string s1 = "ababcabcacbab";
    string s2 = "cabca";
    int len = s2.size();
    int next[len];
    Getnext(next, s2);
    for(int i = 0; i < len; i++) cout << next[i] << ' ';
    cout << endl;
    cout << KMP(next, s1, s2) << endl;
    return 0;
}

代码生成图:

在这里插入图片描述


如有不同见解,欢迎留言讨论~~~

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值