也谈KMP算法

本文深入浅出地介绍了KMP算法的基本原理及其实现方法。首先回顾了朴素模式匹配算法,并指出其存在的问题。随后详细解释了KMP算法如何通过预先计算辅助数组来避免不必要的回溯,从而提高匹配效率。

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

看毛片算法,念书时学过,几年下来也就只记得个名字了。前几天偶然遇到,顺道捡起来摸一摸,还是那手感…… ^_^

 

言归正传,这是个模式匹配算法(好吧,正传第一句居然是废话~)。谈它之前,先说说最简单直观的模式匹配算法——朴素匹配。

 

朴素匹配,就是不动脑子去匹配的算法:

 

源串S[0…n-1]  模式串P[0…m-1] 很显然,n > = m

 

源串和模式串分别有一个指针标记,

对指针指向的源串中每一个位置[0…n-m](n-m向后的不用考虑,剩余长度都不够m了),模式串都是从第一位开始比对,匹配则两个指针同时后移一位;

如果遇到某一位不匹配,源串中开始比较位置后移一位,模式串仍然从第一位开始继续匹配。

 

直至模式串扫描完毕表示匹配 或者源串扫描完毕,表示不匹配。

 

给一个Java实现

	public int StringMatch(String sourceStr, String patternStr) {
		char[] source = sourceStr.toCharArray();
		char[] pattern = patternStr.toCharArray();
		
		int idxSource, idxPattern;
		
		// for each i, check if source[i~...i+m-1] matches pattern[0~...m]
		for(int i = 0; i <= source.length - pattern.length; i++) {
			idxSource = i;
			idxPattern = 0;
			while(idxPattern < pattern.length
					&&source[idxSource] == pattern[idxPattern]) {
				idxSource++;
				idxPattern++;
			}
			
			if(idxPattern == pattern.length)
				return i;
				
		}
		return -1;
	}


简单分析下这个算法,由于外层循环是扫描源串S[0…n-m],内层循环最坏要扫描整个模式串P[m],故算法是O(nm)的。

 

朴素算法里,比如某趟从源串的i = 4,模式串j=0位置开始比对,i和j依次后移,到了i=7 j=3时发现不匹配了,那么下一趟 i又回到5,j继续从0开始匹配下去……

我们发现,源串的指针一直在做前后的“折返跑”(哦,好吧,行话叫“回溯”),有没有办法减少甚至消除此类回溯从而提高效率呢? 额,设问句问出来了,答案当然是有! 而且是本篇猪脚——KMP算法就是基于这种想法而提出的,它消除源串的回溯,是最坏情况O(n)的一个匹配算法!

 

具体说KMP,就是借助一个辅助数组,在匹配过程中一旦出现不匹配,源串指针不用回溯,直接移动模式串继续匹配,直到结束。

 

(注意,由于有形形色色的辅助函数,原理虽然一样,但是边界条件各有不同。各位童鞋如果考试遇到了,请务必按照自己教材的辅助函数计算方式,不然答错了概不负责哈&#**%¥…… 当然话说回来,作者一直认为,数据结构&算法分析啥的考试,让学生手动模拟是很傻很天真的…… 额 貌似又跑偏了)

 

算法思路:

 

如果 S[i-j...i] P[0…j] 在P[j] 失配了,即之前的S[i-j…i-1] = P[0…j-1]  *

但 S[i]<>P[j]

为了达到S串不回溯,需要把模式串P向右移动到某位置(比如K),继续比较S[i] 和 P[k]

 

但是这有一个前提,必须满足P[0…k-1] = S[i-k…i-1] = P[j-k…j-1]  (红色部分是由*式得到的), 否则在P[j]之前的部分就已经不匹配了

 

整理下,就是说如果 S[i-j...i] P[0…j] P[j] 失配了,我们希望得到一个值K,可以让我们的匹配从S[i]P[k]继续尝试下去

 

而K的求法,就是满足P[0…k-1] = P[j-k…j-1]的最大k ( k < j)

 

预定义边界条件 aux[0] = -1 ,如果P[0]失配,S串指针直接后移一位。

 

我们的辅助数组aux[n]定义就是上面的蓝色粗体字,再强调一遍

 

aux[j] = k 表示如果 S[i-j...i] P[0…j] P[j] 失配了,我们可以从S[i]P[k]继续匹配下去。

那么如果S[i] 和 P[k] 还不匹配呢? 再看看上面的定义,P[k]失配, 我们是不是要尝试S[i] 和 P[ P[k] ] ?

没错,重复这个过程,直到遇到S[i]可以和某个P[#] 匹配,或者 #=0还不匹配了,S串指针后移到i+1, P串再重头来过。

(注意,初涉KMP会导致习惯性头晕,个人建议头晕时反复咀嚼蓝色粗体字,效果不错哦~ 实际上,我写这篇的时候也嚼了好几遍的,嚼着嚼着就不晕了 :P)

 

有了aux数组,我们的匹配就可以在源串中一路向西了,哦不,是一路到底哈……

下面给出一个KMP的匹配实现,计算aux数组后面会谈到

(这里注意,预定义 aux[0] = -1)

	public int StringMatchKMP(String sourceStr, String patternStr) {
		// initialize auxiliary array
		int [] auxKMP = auxInitialNonRecursive(patternStr);
//		int [] auxKMP = auxInitialRecursive(patternStr);
		
		char[] source = sourceStr.toCharArray();
		char[] pattern = patternStr.toCharArray();
		
		for(int i = 0; i <= sourceStr.length() - patternStr.length(); i++) {
			int idxPattern = 0;
			while(idxPattern < pattern.length && idxPattern >= 0) {
				if(source[i] == pattern[idxPattern]) {
					i++;
					idxPattern++;
				}
				else { // mismatch in source[i] and pattern[idxPattern]
					idxPattern = auxKMP[idxPattern];
				}
			}
			if(idxPattern == pattern.length)
				return i - pattern.length;
		}
		
		return -1;
	}


下面说说计算aux数组(其取值仅与模式串本身有关)

再嚼一次:aux[j] = k 表示如果 S[i-j...i] P[0…j] P[j] 失配了,我们可以从S[i]P[k]继续尝试匹配。

K的求法,就是满足P[0…k-1] = P[j-k…j-1]的最大K

 

第一种方法,根据 “满足P[0…k-1] = P[j-k…j-1]的最大K”求K

即对每个1 <= j < m  求满足条件的k,如果没有,返回0

(神码?为啥返回0?  头晕嚼一嚼, aux[j] = 0 是不是表明, P[j]失配,我们用P[0]尝试)

 

同样,Java实现如下

	private int[] auxInitialNonRecursive(String patternStr) {
		char[] pattern = patternStr.toCharArray();
		
		int [] aux = new int[patternStr.length()];
		aux[0] = -1;
		
		for(int j = 1; j < patternStr.length(); j++) {
	        // check k from j-1 to 1, does p[0...k-1] matches p[j-k...j-1] ?
            int k = j - 1;
            while(k > 0) {
            	if(checkMax(k, j, pattern)) {
					aux[j] = k;
					break;
				}
            	k--;
            }
            if(k == 0)
            	aux[j] = 0;
		}
		return aux;
	}

	/** check if pattern[0...k-1] matches pattern[j-k...j-1]*/
	private boolean checkMax(int k, int j, char[] pattern) {
		int m = 0;
		int n = j - k;
		
		while(pattern[m] == pattern[n] && m < k) {
			m++;
			n++;
		}
		
		return (m == k);
	}

 

第二种方法,这也是大多数算法书上使用的,递归计算辅助数组

初值还是 aux[0] = -1

假设我们已知 aux[j-1] = k,现在要计算 aux[j]

还是从定义出发, aux[j-1] = k 表示 当P[j-1]失配时,应该移动P串到P[k]开始尝试匹配,而且P[0…k-1] = P[j-k-1…j-2] (这个等式换个角度看其实就是P串自身和自身在做匹配了,当然看不出来也木有关系的)

 

那么,如果 P[k] = P[j-1] 那么P[0…k] = P[j-k-1…j-1] 对比下前面定义,等价于 aux[j] = k+1(这说明当P[j]失配时,移动P串到P[k+1]开始尝试匹配,而且P[0…k] = P[j-k-1…j-1] )

如果 P[k] <> P[j-1] ,相当于 P[k]失配了, 我们令 k' = P[ P[k] ] ,当然也有P[0…k'-1] = P[j-k-1…j-2],

同样,如果P[k'] = P[j-1] 那么P[0…k'] = P[j-k-1…j-1], 等价于 aux[j] = k'+1

重复上面的过程,直到找出aux[j] 或者 某个k = -1,此时 aux[j] = 0

 

递归实现如下

	private int[] auxInitialRecursive(String patternStr) {
		char[] pattern = patternStr.toCharArray();
		
		int [] aux = new int[patternStr.length()];
		aux[0] = -1;
		
		for(int j = 1; j < patternStr.length(); j++) {
			int k = aux[j - 1];
			
			while(k >= 0 && pattern[k] != pattern[j - 1])
				k = aux[k];
			
			aux[j] = (k < 0) ? 0 : k + 1;
		}
		
		return aux;
	} 

可以借助平摊分析,得出KMP算法是O(n)的时间复杂度,至此,KMP算法就算谈完了

总结一下,

  1. KMP分两个部分

求辅助数组 & 应用辅助数组进行串匹配

  1. 通过使用辅助数组,KMP消除源串的回溯,(严格说,还是有原地踏步匹配的,但没有回头重复匹配的);

模式串是有回溯的——但未必每次都要回到第一位开始匹配,这个取决于辅助函数的返回

 

 

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值