KMP算法详解

本文深入浅出地介绍了经典的KMP字符串匹配算法,包括其由来、核心思想、具体实现及Java代码示例。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

                            一.字符串匹配

      KMP算法,看了也有一天了,感觉这个算法真是相当的难,但是也非常的经典,看了许多关于KMP的博客和《算法导论》才算简单的理解了这个算法,感觉书上讲的KMP 适不适合初学者的,这里我就用通俗的语言说一下我理解的KMP。说道字符串匹配,也就是这一样,给出一个T文本字符串,再给出一个P模式字符串,在T中找到模式第一次出现的位置。真如下面:

 

              二.KMP的由来

        这么经典的算法想想也知道肯定不是某个人突然就想出来了,而是根据原来算法的缺点不断改进,发现其中的缺陷,找出规律。最早的字符串匹配算法也就是 朴素算法。此算法是利用双指针每次匹配不成功就回溯到当前开始匹配的地方+1处,继续匹配。如图:

     


      显然这种算法是很不效率的,用上面的图不方便说明,我们用图下说明:

当匹配不成功时,用肉眼就可以看出来,是没必要从T2(也就是b字符)开始进行比较的,正确的无冗余的地方应该是T3位置。

        因为,前两个字符已经匹配成功,并且P[1]!=P[2] ,所以很明显没必要比较一遍'a'=='b'?,所以直接从T3位置匹配,那末是如何发现这种能用肉眼看出来的信息呢?我们下面会讲。

        朴素字符串算法因为做一些冗余的回溯,所以时间复杂度为 O((n-m+1)m),(n为T的长度,m为P的长度),当m=1/2n,时,此算法就变成了平方 的级别,所以是很低效的。

这里给出 朴素算法的java代码:

 

public int strStr2(String haystack, String needle) {
    	char[]a=haystack.toCharArray();
    	char[]b=needle.toCharArray();
    	if(needle=="")  return 0;
    	int i=0,j=0;
    	for(;i<a.length && j<b.length;){
    		if(a[i]==b[j]){
    			i++;j++;
    		}
    		else{
    			i=i-j+1;//下一次开始的位置,就为这个位置不用另一个变量 这是有规律的
    			j=0;
    		}
    	}
    	return j>=b.length ? i-j :-1;
    }




        字符串匹配一直以来就被先人研究者,因此又有了,Rabin-karp  算法,有限自动机算法,以及最经典的最快的KMP算法。这里只介绍KmP算法。只给出所有算法的时间复杂度:想研究的可以看一下《算法导论》32章。


       三.KMP算法

               KMP算法,由D.E.Knuth与V.R.Pratt和J.H.Morris同时发现,因此人们称它为克努特——莫里斯——普拉特操作(简称KMP算法)。通过一个辅助函数实现跳过扫描不必要的目标串字符,以达到优化效果。没错,这个辅助函数就是给我们了上面提到的有用信息。KMP算法的大体结构其实就是:实现两次对P字符串的匹配。从而达到m+n的时间。

            1.前缀函数next

      算法导论中把辅助函数也就是前缀函数用next表示,思路:使模式字符串,与本身进行匹配,next数组记录匹配结果,即到P[i]为止的前缀和后缀的最长公共部分,前缀的范围为1——(i-1),是不能超过i长度的。

  可以对照下面的π数组理解这个数组存放的什么。

 

       这里先给求解next函数(π)的伪代码:

         

      当看到这个函数时,大部分人肯定都很快理解了当P[K+1]==P[q]的时候,这就是前缀和后缀匹配成功的时候,继续前缀加一个,后缀再加一个字符的判断呗,这里需要注意的是:下表的范围当i==1的时候,π[i]=0,0在数组中是一个无效的数字(仅限于算法导论中,的下标表示方法,若你想使0有意义,可以π[i]=-1,0表示P0位置的字符串是P[i]的后缀),条条大路通罗马,不管哪一种表示,只要注意一下边界就行了。

      当P[K+1]!=P[q]的时候,这也是本算法最难理解的部分,也是此算法的精髓,其实当匹配失败的时候,他总是在寻找比匹配失败前的公共长度的最大值,即,当p[i]匹配失败,当前的(p[i-1])最大公共前后缀的长度为 k:

   p[1]------p[k]与  p[i-k]------p[i-1]是相同的,既然p[i]匹配失败所以之前的 最大长度 不能用了,所以要找一个同样是 p[1]开头 p[k]结尾的字符串,所以与k位置相同的字符就是 π[π[k]] 位置的字符,可以对照上面的图进行理解。这里给出一个例子。

         不妨设模式串Pattern = "a  b  c  c  a  b  c  c  a  b  c  a"

                Pattern 数组编号: 0  1  2  3  4  5  6  7  8  9 10 11
NOCM 表示 已经匹配的字符数
LOLP 表示 既是自身真后缀又是自身最长前缀的字符串长度
以下是计算流程:
PrefixFunc[1] = 0; //只匹配一个字符就失配时,显然该值为零
LOLP = 0;   NOCM = 2;   LOLP = 0;    PrefixFunc[2] = 0;
LOLP = 0;   NOCM = 3;   LOLP = 0;    PrefixFunc[3] = 0;
LOLP = 0;   NOCM = 4;   LOLP = 0;    PrefixFunc[4] = 0;
LOLP = 0;   NOCM = 5;   LOLP = 1;    PrefixFunc[5] = 1;
LOLP = 1;   NOCM = 6;   LOLP = 2;    PrefixFunc[6] = 2;
LOLP = 2;   NOCM = 7;   LOLP = 3;    PrefixFunc[7] = 3;
LOLP = 3;   NOCM = 8;   LOLP = 4;    PrefixFunc[8] = 4;
LOLP = 4;   NOCM = 9;   LOLP = 5;    PrefixFunc[9] = 5;
LOLP = 5;   NOCM = 10; LOLP = 6;    PrefixFunc[10] = 6;
LOLP = 6;   NOCM = 11; LOLP = 7;    PrefixFunc[11] = 7;
LOLP = 7;   NOCM = 12;

---------此时满足条件while( LOLP>0 && (Pattern[LOLP] != Pattern[NOCM-1]) )-------------

while语句中的执行
{
           LOLP = 7;   NOCM = 12;  LOLP = PrefixFunc[7] = 3;
           LOLP = 3;   NOCM = 12;  LOLP = PrefixFunc[3] = 0;
}

LOLP = 0;   NOCM = 12; LOLP = 1;    PrefixFunc[12] = 1;
最后我们的前缀函数 PrefixFunc[] = { 0,0,0,0,1,2,3,4,5,6,7,1 }
其间最精妙的要属失配时的操作
while( LOLP>0 && (Pattern[LOLP] != Pattern[NOCM-1]) )
              LOLP = PrefixFunc[LOLP];
其中 LOLP = PrefixFunc[LOLP];  递归调用PrefixFunc函数,直到整个P字串都再无最长前缀或者找到一个之前的满足条件的最长前缀。
        2.KMP匹配

       既然辅助函数已经完成,那么就可以利用辅助函数去在T中寻找最快最有效的偏移,你可能会问仅凭对P的自身匹配就能找出来,最有效的偏移位置? 没错,是的,因为P与T已经部分匹配成功,匹配成功的部分与T中的那部分是一样的字符串。图下说明:

       

上图匹配到 D与空格的时候 ,匹配失败那么 下一个有效的开始匹配的偏移就是:

        s=q-π[6]=6-2=4             q :当前已匹配的最大长度。

其实本质上就是寻找 下一个位置,后面那个位置与P的前缀一致。下面给出伪代码:

 

              四 . KMP java代码

          

public int strStr(String T,String P){
           if(P=="")
    		return 0;
    	char t[] = T.toCharArray();
    	char p[] = P.toCharArray();
    	int []next = next(P);
    	int q=-1;
    	for(int i = 0;i<t.length;i++){
    		while(q>=0 && p[q+1]!=t[i])//开始迭代用于当已经有成功的匹配时,迭代寻找最大的
    			q=next[q];            //若不加q>=0,若第一个字符不相等会越界错误
    		if(p[q+1] == t[i])
    			q++;
    		if(q==p.length-1)
    			return  i-q;
    	}
    	return -1;
    }
    /**
     * KMP算法的一个辅助函数,next也称为 是前缀函数,求p[i]的
     * 最大公共前缀和后缀的长度
     */
    private int [] next(String p){
    	char s[] = p.toCharArray();
    	int []next = new int[s.length];
    	next[0] = -1;//与空没有公共部分
    	int  j = -1;
    	for(int i=1;i<s.length;i++){
    		//这个while循环是KMP最经典的部分也是最难理解的部分
    		while(j>=0 && s[j+1]!= s[i]) j=next[j];
    		if(s[j+1]==s[i])
    			j++;
    		next[i]=j;
    	}
    	return next;
    }


 


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值