Java数据结构之KMP算法怎么实现


这篇文章主要讲解了“Java数据结构之KMP算法怎么实现”,文中的讲解内容简单清晰,易于学习与理解,下面请大家跟着小编的思路慢慢深入,一起来研究和学习“Java数据结构之KMP算法怎么实现”吧!这是最常见的算法字符串匹配算法,暴力匹配也叫朴素匹配。思路很简单,从主串的第i个字符开始遍历,依次与子串的每个字符进行匹配,如果某个字符匹配失败,则主串回溯第i+1个字符,子串回溯到第1个字符,重新开始匹配,直到遍历完主串匹配失败或者遍历完子串匹配成功。很明显这种算法需要在一个双重for循环中实现,时间复杂度为O(m*n),m为主串长度,n为子串长度。随着字符串长度的增长,时间复杂度快速上升。Java中字符串的contains方法实际上就是采用的BF算法。KMP 算法是 D.E.Knuth、J,H,Morris 和 V.R.Pratt 于1977年共同提出的,称之为 Knuth-Morria-Pratt 算法,简称 KMP 算法。KMP算法是一种改进的字符串匹配算法,核心是利用之前的匹配失败时留下的信息,选择最长匹配长度直接滑动,从而减少匹配次数。KMP 算法时间复杂度为O(m+n),m为主串长度,n为子串长度。BF匹配失败之后,主串和子串都会最大回溯,但是很多时候都是没有必要的。例如对于主串abababcd,子串ababc,第一次匹配之后,很明显主串和子串会匹配失败,但是我们能够知道他们的能够匹配的前缀串,即abab:如果在第二次匹配的时候,主串不回溯,子串滑动两个字符长度,那么我们就能在第二次的时候实现匹配成功。到这里,这种加速的方法已经呼之欲出了,但是我们先介绍两个重要概念:1.匹配前缀:在某一次主串和子串的匹配失败之后,前面匹配成功的那部分子串就被称为匹配前缀。这就是一次匹配失败时留下的信息。例如主串abcde,子串abcc,那么在第一次匹配的时候,匹配前缀为abc。2.最长匹配长度:对于每次匹配失败后的匹配前缀串,其前缀子串(连续,且一定包括第一个字符,不包括最后一个字符)和后缀子串(连续,且一定包括最后一个字符,不包括第一个字符)中,相同的前后缀子串的最长子串长度,此时的前缀、后缀字串也被称为最长真前缀、后缀子串。例如匹配前缀abc,没有匹配的前缀和后缀,那么其最长匹配长度为0。例如匹配前缀cbcbc,最长匹配的前缀和后缀子串为cbc,那么其最长匹配长度为3。例如匹配前缀abbcbab,最长匹配的前缀和后缀子串为ab,那么其最长匹配长度为2。有了这两个概念,那么我们才能进行跳跃式滑动,对于主串,在匹配失败的位置不进行回溯,对于子串,则是回溯(滑动)到其匹配前缀的最长匹配长度的位置上继续匹配,这样就跳过了之前的部字免费云主机域名符串的匹配,且只需要匹配剩下的部分字符串即可。我们再详细解释下,这里子串跳过的到底什么?实际上它跳过的就是匹配前缀串的最长匹配长度串。
设主串abababcd,子串ababc,第一次匹配失败之后,主串匹配索引i=4,子串匹配索引j=4,此时匹配的相同前缀串为abab,它的最长匹配长度为2,即最长前缀串ab和最长后缀串ab。那么第二次匹配之前,字串匹配索引j直接跳到第一匹配的相同前缀串的最长匹配长度的索引位置上即j=2。我们可以这么理解,主串的第一次匹配的相同前缀串的最长匹配后缀,与子串第一次匹配的相同前缀串的最长匹配前缀相等(或者说重合)。这是我们在底层一次失败匹配之后得到的有效信息,在第二次匹配时自然可以利用起来,利用最长的前后缀匹配信息,跳过这些多余的匹配,实现加速。(后续学习的AC自动机也是采用了前后缀匹配的思想)这就是KMP算法加速的核心原理,每次匹配失败之后,利用匹配失败的信息,找到最长匹配长度,然后主串不回溯,子串尽可能少的回溯,相比于BF算法,减少了没必要的匹配次数。基于上面的原理,我们知道可能会不止一次查找最长匹配长度,而且我们会发现,最长匹配长度的范围只能在子串长度范围之内,而且其计算结果只和子串有关。那么我们就可以先初始化一个数组,用来保存不同长度的前缀的最长匹配长度。这就是所谓的next数组,也被称为部分匹配表(Partial Match Table),也是KMP算法的核心。next数组的大小就是子串的长度,每个的索引位置i表示长度为i+1的子串的匹配前缀子串,值v表示对应匹配前缀子串的最长匹配长度。假设子串为ababc,那么next数组值为:假设子串为abcabdabcabc,那么对应的next数组如下:其实很好理解:现在,我们的首要问题变成了求next数组。首先,切next数组的问题实际上就是求最大的前、后缀长度的问题,那么我们可以使用最朴素的方式求解:不难发现,求解next数组的时间复杂度为O(n^2),是否有更快速的方法呢?当然有,可以发现,在求next[i]的最长匹配长度的时候,next[0], next[1], … next[i-1]的结果已经求出来了。因此我们尝试利用此前的结果直接推导出后面的结果。下面是分情况讨论。设子串为str=ababc,i=3,那么next[i-1]=1,即子串aba的的最长匹配长度为1,那么str[next[i-1]]实际上就是最长匹配子串前缀后一个字符,即str[1]=b。如果str[i]=str[next[i-1]],就相当于在前一个子串的最长匹配长度的基础上增加了一位,即next[i]=next[i-1]+1。如下图:如果str[i]!=str[next[i-1]],此时就会复杂一些。此时我们需要缩短最长匹配子串的长度,具体怎么缩短呢?设str = abcabdabcabc,设i = 11,即最后一个字符c,那么next[i-1] = 5,但是由于str[i] != str[next[i-1]],即d != c,那么此时我们需要求i-1的最长匹配长度子串abcab的最长匹配长度子串,即next[next[i-1]-1] = 2,然后判断str[i]是否等于str[next[next[i-1]-1]],如果相等则同第一种情况,否则继续缩减直到next[next[i-1]-1]为0为止,此时表示当前子串的最长匹配长度也为0。如下图:基于上面的规律,我们的改进算法如下:这种算法的时间复杂度为O(n),大大缩短了求next数组的时间。有了next数组,那么KMP算法就很容易实现了。使用i和j分别表示主串和子串的匹配进度,i永远不会回退,依次匹配主串和子串的字符:1.如果字符相等则推进i、j,并且判断如果匹配到了一个完整的子串,那么返回起始索引。2.如果不相等:如果当前子串进度为0,那么子串不需要回退,主串向后推进i,重新开始匹配;如果当前子串进度不为0,那么子串进度需要回退到next[j-1]的位置,此前的位置不再需要匹配,主串不需要向后推进i,随后重新开始匹配。如果i进度匹配完毕,那么退出循环,表示没有匹配到任何完整的子串,返回-1。上面我们的实现是返回第一个匹配到的模式串的起始索引,那么如果我们需要返回所有匹配到的模式串的起始索引呢?
其实也很简单。在每次匹配某个字符成功之后判断,如果匹配到了一个完整的子串,那么我们求起始索引并且加入结果集,然后子串点位j需要回退,继续循环。感谢各位的阅读,以上就是“Java数据结构之KMP算法怎么实现”的内容了,经过本文的学习后,相信大家对Java数据结构之KMP算法怎么实现这一问题有了更深刻的体会,具体使用情况还需要大家实践验证。这里是百云主机,小编将为大家推送更多相关知识点的文章,欢迎关注!

相关推荐: php decode乱码如何解决

这篇文章主要讲解了“php decode乱码如何解决”,文中的讲解内容简单清晰,易于学习与理解,下面请大家跟着小编的思路慢慢深入,一起来研究和学习“php decode乱码如何解决”吧! php decode乱码是因为“json_encode()”函数只能编码…

免责声明:本站发布的图片视频文字,以转载和分享为主,文章观点不代表本站立场,本站不承担相关法律责任;如果涉及侵权请联系邮箱:360163164@qq.com举报,并提供相关证据,经查实将立刻删除涉嫌侵权内容。

Like (0)
Donate 微信扫一扫 微信扫一扫
Previous 02/25 18:18
Next 02/25 18:18

相关推荐