字符串匹配——KMP算法

 字符串匹配的问题是:在主串中匹配模式串,并返回匹配到模式串的起始位置。

 蛮力搜索O(m*n)的时间复杂度,重复匹配步骤太多。KMP算法的核心是发现模式串中的“前缀=后缀”,避免在模式串j位置发生匹配失败时还要回退到0位置开始匹配。 发现“前缀=后缀”,只需要将模式串向前移动合适的位置,跳过了重复匹配前缀的步骤。

所以,KMP算法使用了一个next数组来保存模式串中的前缀=后缀的最大的k值,next[j]表示模式串的子串P0...Pj-1中使得P0...Pk-1 = Pj-k...Pj-1即前缀=后缀的最大k。构造next数组是KMP算法的核心工作。至于匹配的过程,主串依次扫描来匹配模式串,当模式串中的j位置匹配失败时,就让j=next[j]接着匹配,每次匹配失败后跳过合适的位置,也就是避免了再重复匹配前缀。

详细的分析可以阅读 http://www.cnblogs.com/huangxincheng/archive/2012/12/01/2796993.html

http://www.cnblogs.com/goagent/archive/2013/05/16/3068442.html

在下面这个例子中,第一轮当主串扫描到i位置与模式串的j位置没有匹配成功,此时模式串的子串aba存在“前缀=后缀”的情况也就是a=a,所以下一轮前缀a已经不用再比较了,直接去比较了b:


next[j]来记录失配时模式串应该用哪一个字符于Si进行比较。
设 next[j]=k。根据公式我们有
                -1        当j=0时

next[j] =   max{k| 0<k<j 且 P0P1..Pk-1=Pj-kPj-k+1...Pj-1}
                0         其他情况
好,接下来的问题就是如何求出next[j],这个也就是kmp思想的核心,对于next[j]的求法,我们采用递推法,现在我们知道了next[j]=k,我们来求next[j+1]=?的问题?其实也就是两种情况:
①:Pk=Pj 时  则P0P1...Pk=Pj-kPj-k+1...Pj, 则我们知:
               next[j+1]=k+1。
    又因为next[j]=k,则
             next[j+1]=next[j]+1。

②:Pk!=Pj 时  则P0P1...Pk!=Pj-kPj-k+1...Pj,这种情况我们有点蛋疼,其实这里我们又将模式串的匹配问题转化为了上面我们提到的”主串“和”模式串“中寻找next的问题,你可以理解成在模式串的前缀串和后缀串中寻找next[j]的问题。现在我们的思路就是一定要找到这个k2,使得Pk2=Pj,然后将k2代入①就可以了。
 设   k2=next[k]。 则有P0P1...Pk2-1=Pj-k2Pj-k2+1...Pj-1。
 若   Pj=Pk2,      则 next[j+1]=k2+1=next[k]+1。
 若   Pj!=Pk2,      则可以继续像上面递归的使用next,直到不存在k2为止。

Java代码实现如下:

/**
 * 在主串中匹配模式串,并返回匹配到模式串的起始位置
 * 蛮力搜索O(m*n)的时间复杂度,重复匹配步骤太多
 * KMP算法的核心是发现模式串中的“前缀=后缀”,避免在模式串j位置发生匹配失败时还要回退到0位置开始匹配
 * 发现“前缀=后缀”,只需要将模式串向前移动合适的位置,跳过了重复匹配前缀的步骤
 * @author pixel
 *
 */
public class KMP {
	public static int kmp(String majorString, String pattern)
	{
		if (majorString == null || pattern == null || majorString == "" || pattern == "")
			return 0;
		int[] next = makeNext(pattern);
		int i = 0, j = 0;
		//只扫一遍主串并且不回退,每次当匹配模式串中的pattern[j]失败时,就将模式串前移next[j],并且避免了重复匹配
		while (i < majorString.length() && j < pattern.length())
		{
			if (j == -1 || majorString.charAt(i) == pattern.charAt(j))
			{
				i ++;
				j ++;
			}
			else
			{
				j = next[j];
			}
		}
		if (j == pattern.length())
			return i - j;
		return -1;
	}
	/**
	 * 构造next数组,next[j]表示:
	 * 为了寻找在pattern串的子串P0...Pj中,
	 * 前缀P0 P1 ... Pk-1 = 后缀Pj-k Pj-k+1 ... Pj时,最大的k值
	 * @param pattern
	 * @return next数组
	 */
	private static int[] makeNext(String pattern)
	{
		int[] next = new int[pattern.length()];
		int k = -1;
		int j = 0;
		//当j=0时next[j]为-1
		next[j] = -1;
		//递推next[j+1]
		//next[j]是在P0...Pj-1中找前缀=后缀的最大k使得P0...Pk-1 = Pj-k...Pj-1
		//所以next[j+1]就是在P0...Pj中找,而next[j]时已经判断了P[k-1]=P[j-1],现在只需判断P[k]?P[j]
		while (j < pattern.length() - 1)
		{
			//Pk == Pj的情况:next[j+1]=k+1 也就是 next[j+1]=next[k]+1
			if (k == -1 || pattern.charAt(k) == pattern.charAt(j))
			{
				next[++j] = ++k;
			}
			else
			{
				//Pk != Pj的情况:递推k = next[k],不断在上一次的前缀中寻找Pk=Pj
				k = next[k];
			}
		}
		return next;
	}
	
	public static void main(String args[])
	{
		String major = "aababcdad";
		String pattern = "abcd";
		System.out.println(kmp(major, pattern));
	}
}


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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值