KMP算法个人理解

参考的原文:详解KMP算法
KMP:解决字符串匹配问题,字符串s是否是主串的子集?是,返回第一个元素坐标;否,返回-1。
核心思想,尽量不移动主串的i指针,回溯子串的j指针。

整个KMP的重点就在于当某一个字符与主串不匹配时,我们应该知道j指针要移动到哪?
接下来我们自己来发现j的移动规律:

在这里插入图片描述

如图:C和D不匹配了,我们要把j移动到哪?显然是第1位。为什么?因为前面有一个A相同啊:

在这里插入图片描述

如下图也是一样的情况:
在这里插入图片描述

可以把j指针移动到第2位,因为前面有两个字母是一样的:

在这里插入图片描述

至此我们可以大概看出一点端倪,当匹配失败时,j要移动的下一个位置k。存在着这样的性质:最前面的k个字符和j之前的最后k个字符是一样的。

如果用数学公式来表示是这样的

P[0 ~ k-1] == P[j-k ~ j-1]
如图
在这里插入图片描述
至此得出一个问题:为什么能把j指针移动到k位置上
首先,当T[i] != P[j]时,必有T[i-j ~ i-1] = P[0 ~ j-1]
因为K必定小于j,所以得出 T[i-k ~ i-1] = P[j-k ~ j-1]
又因K满足P[0 ~ k-1] = P[j-k ~ j-1]
得 T[i-k ~ i-1] = P[0 ~ k-1]
所以能把j指针移动到k位置

关于K的描述还有前缀集和后缀集:
该规律是KMP算法的关键,KMP算法是利用待匹配的子串自身的这种性质,来提高匹配速度。该性质在许多其他中版本的解释中还可以描述成:若子串的前缀集和后缀集中,重复的最长子串的长度为k,则下次匹配子串的j可以移动到第k位(下标为0为第0位)。我们将这个解释定义成最大重复子串解释。

这里面的前缀集表示除去最后一个字符后的前面的所有子串集合,同理后缀集指的的是除去第一个字符后的后面的子串组成的集合。举例说明如下:

在“aba”中,前缀集就是除掉最后一个字符’a’后的子串集合{a,ab},同理后缀集为除掉最前一个字符a后的子串集合{a,ba},那么两者最长的重复子串就是a,k=1;

在“ababa”中,前缀集是{a,ab,aba,abab},后缀集是{a,ba,aba,baba},二者最长重复子串是aba,k=3;

在“abcabcdabc”中,前缀集是{a,ab,abc,abca,abcab,abcabc,abcabcd,abcabcda,abcabcdab},后缀集是{c,bc,abc,dabc,cdabc,bcdabc,abcdabc,cabcdabc,bcabcdabc},二者最长重复的子串是“abc”,k=3;
详见KMP详解 第二节

求next数组

先贴代码

public static int[] getNext(String ps) {

    char[] p = ps.toCharArray();

    int[] next = new int[p.length];

    next[0] = -1;

    int j = 0;

    int k = -1;

    while (j < p.length - 1) {

       if (k == -1 || p[j] == p[k]) {

           next[++j] = ++k;

       } else {

           k = next[k];

       }

    }

    return next;

}

可以发现next[0] = -1,next[1] = 0必定成立。
先来看第一个:当j为0时,如果这时候不匹配,怎么办?j不动,移动i
在这里插入图片描述
再看第二个:j=1时,前面只有一个元素,所以只能回滚到这个坐标
在这里插入图片描述
最后有两种情况要重点讨论:
1.P[k] == P[j]
2.P[k] != P[j]

分别讨论

1.P[k] == p[j]时

在这里插入图片描述
当指针指在j时有个前提条件:P[0 ~ k-1] == P[j-k ~ j-1]
因为P[k] == P[j] 所以 P[0 ~ k] == P[j-k ~ j]
因此next[j+1] == k + 1 == next[j] + 1

2.P[k] != P[j]

代码中写的是 k = next[k] 如何理解 (最难理解的点
引用
当指针指向j时,有P[0~k-1] = P[j-k ~ j-1]
当指针指向j+1时,设此时的k为k1,则有P[0 ~ k1-1] = P[j+1-k1 ~ j]
因为P[j] != P[k] 所以 k1 <= k
那么k1具体等于多少呢?不知道,先从最大值k开始排查。
当k1=k时,有P[0~k-1] = P[j+1-k ~ j]
分解P[0~k-2] + P[k-1] = P[j+1-k ~ j-1] +P[j]
又P[j+1-k ~ j-1] = P[1 ~ k-1]
所以再分解为:
①P[0~k-2] = P[1 ~ k-1]
这就是求next[k]时最先判断的字符串,
②P[k-1] = P[j]

①式如果不成立,那么需要令k1=k-1,还得再判断一遍。不如跳过这些无用的假设值,直接让①式成立,去判断②成不成立。
如此便推出了代码中的k = next[k] 再判断P[k] == P[j]
如果P[k] == P[j]不成立,那么再来k1<= next[k],属于迭代的问题,再令k=next[k],继而推出代码

while(...){
	if ( k == -1 || P[j] == P[k]){
		P[++j] = ++k;
	}else {
		k = next[k];
	}
}

有了Next数组后,就可以写出完整的KMP算法了

public static int KMP(String ts, String ps) {

    char[] t = ts.toCharArray();

    char[] p = ps.toCharArray();

    int i = 0; // 主串的位置

    int j = 0; // 模式串的位置

    int[] next = getNext(ps);

    while (i < t.length && j < p.length) {

       if (j == -1 || t[i] == p[j]) { // 当j为-1时,要移动的是i,当然j也要归0

           i++;

           j++;

       } else {

           // i不需要回溯了

           // i = i - j + 1;

           j = next[j]; // j回到指定位置

       }

    }

    if (j == p.length) {

       return i - j;

    } else {

       return -1;

    }

}

以上算法还有优化,详见开头链接

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值