前言
关于KMP算法的描述在网上可以说是多如牛毛,以前学习的时候也碰到过这个问题。只是一直对它的理解不够深刻。而在网上搜索了一通之后,发现大量的文章要么就是简单的说一下思路然后给一堆代码,要么就是纯粹讲理论,对于实际的实现没有任何帮助。自己在学习和实现整个算法的过程中也碰到过几个小的细节,被卡在那里很久。经过很久的揣摩才想清楚了一点,这里就把整个算法的思想和实现过程详细描述一下。希望能够带来一点帮助。
最初始的方法
我们在讨论KMP算法之前,先从一个最原始的思路开始考虑。这是一个检查字符串是否匹配的问题。假定我们有一个源字符串t和需要匹配的模式串pattern。我们希望就是在字符串t里找到一个匹配模式串pattern的地方。我们有一种最简单直接的思路,就是每次去从头比较两个串,如果发现一旦中间有一个不同,则从目标串的后面一个开始,重新去和模式串从头开始比较。这个比较的过程如下图:
从图中我们可以看到,在a的情况下,我们匹配了第一个字符,然后第二个不匹配,然后我们就从目标串的c字符开始,再来看模式串里第一个。在b图中,也不匹配,然后我们继续移动到目标串的下一个。一直到图c的情况下,我们发现它们匹配了。于是找到了一个完整的匹配结果,并可以将这个匹配的位置返回。
按照这个思路,我们也可以很容易得到一个实现,其代码如下:
public static int search(String pat, String txt) { int m = pat.length(); int n = txt.length(); for(int i = 0; i <= n - m; i++) { int j; for(j = 0; j < m; j++) { if(txt.charAt(i + j) != pat.charAt(j)) break; } if(j == m) return i; } return -1; }
在这个方法里,我们假定找到一个匹配的串,则返回和目标串匹配的第一个字符的索引,否则返回-1。整个过程还是很简单的。整个算法的执行时间复杂度为O(mn),m,n分别为两个串的长度。这个方法好在足够简单直观。可是在一些比较大的文件里搜索模式串的话,还是可能会有一定的性能影响。那么,我们有没有可能改进一下算法来提高性能呢?
第一步的观察
我们再结合一个实例来运行一下前面的代码看看:
在前面示例里,当i为0的时候,我们的模式串j匹配到2的时候失败了。这样我们就匹配了前面两个字符AB。可是在算法里,接着从i=1这个位置开始。其实我们都已经看到了,既然前面两个字符是AB,我们再去从B来做比较都没什么意义了。因为它肯定不一样。其实我们完全可以直接跳过它。
我们再从一个笼统的角度来考虑一下字符串匹配的算法。其实我们要比较一个目标串和模式串是否匹配,无非就有如下这么几种情况。一种就是一个字符都不匹配,就好比前面方法中我们模式串里第一个字符和目标串比较就发现不匹配。还有一种就是完全匹配,那就是我们一个个的比较过来,到最后,发现它们都符合,然后这就是我们所期望的结果。当然,更多的情况可能就是我们只是匹配了一部分结果,然后发现后面的不匹配了。假定我们的模式串长度为n,那么我们可能出现部分匹配的情况就有n-1个,比如说1个字符匹配,2个...n-1个匹配。对于一个都不匹配和完全匹配我们都好理解,反正一个个过来,从前面算法来看,就是一个直接的线性时间。它们几乎没有什么改进的地方。所以我们问题的重点就在于针对有一部分串匹配的时候,怎么来改进它使得算法的性能更佳。从前面的分析我们刚才也看到了,确实对于一些部分匹配的情况来说,有的比较很明显,我们可以直接跳过去的。现在的问题关键在于,这些情况是个什么样的规律呢?我们该怎么来跳过去?
进一步推导
我们来看一个具体的匹配过程:
假定我们的目标串T和模式串P匹配检查到如图的情景。这个时候,我们两个串中间前面5个字符是匹配的,就第6个不匹配。如果我们要寻找下一个去匹配的点,将P从头开始去和后面那个目标串的比较的话,第一个比较的是b,不行,我们可以直接跳过去。那我们再往后面看一点,下一个字符是a,和模式串的第一个匹配,再后面的两个字符ba也和模式串里接着的第二三个字符匹配。就好比如下的情况:
实际上,针对这个情况,我们刚才发现前面的三个字符已经和目标串匹配了的。我们完全可以从模式串的第4个开始和目标串进行比较。而且前面几种情况我们跳过了是因为它们第一个字符就不匹配,可以根本不用比较,而这个的情况是有一部分字符是匹配的,可能在目标串的后面那一段和我们模式串的第三个字符以后的部分匹配。所以这部分我们是不能忽略的。
唔...这个时候,我们好像发现一点点什么规律了。问题的关键在于我们前面匹配了一部分字符串,然后我们又用模式串去和匹配了的那部分做比较,选择一些我们两者覆盖的部分,然后再从后面进行比较。前面这个问题中覆盖的部分如下:
这个覆盖很有意思,它是我们前面部分匹配串的结尾,同时又是和我们模式串的开头是相同的。和我们前面那种原始搜索的办法比起来,我们是希望跳过不需要的比较,同时也希望不能错过一些匹配的位置。假设我们从某个部分匹配的位置开始,模式串想对于开头的位置越近而且它们能够满足前面的开头和结尾匹配的条件的话,它们匹配的那部分也越长。所以严格来说,我们相当于找到一个最长而且相等的前缀和后缀。当我们找到这部分相等的部分时,我们就不需要从头再来比较了,而且也不需要像原来的算法中那样目标串退回到这一次第一个比较的字符的后面,而是直接继续往后面比较。比如说前面示例中我们发现了aba这个部分,那么后面目标串就只要看接着的字符是不是等于模式串中间的第4个就可以了。
结合我们前面的这一段讨论,我们可以找到一个部分匹配串里如何往后面进行选择比较的规律了。我们只要针对各种部分匹配的情况来考察它们是否有是否相等的前缀和后缀,然后来选择进行比较就好办了。我们前面针对的各种部分匹配情况,不管是1个匹配还是n-1个字符匹配,这些匹配的串其实本质上就是模式串的一部分。这样看起来似乎和目标串到没什么关系了。按照我们前面的分析,在上个问题中,我们匹配了5个字符的时候,发现有3个字符的前缀和后缀是相等的。于是后面我们就从第4个开始继续进行比较。如果我们匹配的是4个,3个,或者其他的呢?既然这种情况是定死了的,我们只要推导出来各种情况,以后每次直接从匹配的部分往后比较不就可以了吗?这样看来,我们只要建立这么一个前后缀匹配串的表就好办了。我们先拿一个模式串来推导一下:
在图中,我们将模式串ababaca和每次部分匹配的情况做了一个对比。对于每个匹配的情况,和模式串重合的部分表示相互涵盖的地方。图中字符串红色的部分表示模式串剩余的部分。在前面只有一个字符a的情况下,因为我们希望的是要有前缀和后缀,而对于这种情况,我们可以认为它们没有前缀和后缀,所以返回匹配的字符个数为0。假定我们也按照前面的对应某个字串它有多少涵盖的地方,设为M[i]的数组,很容易得到一个如下的表:
i | 0 | 1 | 2 | 3 | 4 | 5 | 6 |
P[i] | a | b | a | b | a | c | a |
M[i] | 0 | 0 | 1 | 2 | 3 | 0 | 1 |
实现
有了前面的充分讨论,我们可以考虑一下该怎么实现一个如下M[i]数组。在这个基础之上我们再考虑如何实现整个的算法。我们先来看第一部分。
前缀覆盖实现
现在我们需要的是针对各种长度的字串来考虑实现。对于长度为1的字串来说,肯定结果为0, 可以直接跳过去。对于长度为2的来说呢?我们需要比较的是第2个元素和第1个是否相等,如果相等的话,则有一个覆盖的元素。假如我们前面已经找到覆盖的若干个元素了,在后面接着的那个元素又不匹配,那我们该怎么来调整和计算覆盖了的长度呢?这是这个问题里最关键的部分。以下图为例:
假定我们用j来表示覆盖的字符长度,在前面已经覆盖了3个的情况下,我们看第4个的时候发现已经不匹配了。这个时候我们就需要回退j,这个j该回退到哪里好呢?从0开始?在这个问题中,我们还有一种情况是可能找到匹配覆盖的:
这个场景比较有意思。在前面我们发现第4个不匹配的情况下,我们至少知道前面是已经匹配了3个的。我们需要的是从这3个里回退到某个点来比较后面一个字符和前面不匹配的。最有意思的地方在于,我们这里不又回到前面找覆盖的子问题吗?至少前面我们已经找出来3个的元素里覆盖的是1。这里就退到了1这个位置来和前面的做比较。更巧合的是1表示3个里覆盖了一个,那么以这个来继续往后面比较的元素它的索引也正好是1。和这种情况一致的场景如下图:
在前面我们匹配了3个之后发现第4个不匹配了,然后跳到j = 1开始比较。这里正好又匹配上了。匹配上之后我们就不用再往后面退了,只需要在原来这个的基础上加1表示这种情况下匹配的字符个数。
所以,上面总结起来就是这么一个过程,当我们匹配到某个字符,假设到第j个了。这时发现它不匹配,我们就将j回退到M[j - 1],如果这个时候它和目标字符相同了,则表示这个长度匹配的字符串长度为M[j - 1] + 1。否则我们就继续往回退。我们这里不断回退肯定是一个循环。而退出这个循环的条件是j == 0或者M[j - 1]这个位置的字符和目标字符相等了。所以我们可以得到一个如下的实现代码:
public static int[] computePrefix(final String s) { int size = s.length(); int[] result = new int[size]; int j = 0; for(int i = 1; i < size; i++) { while(j > 0 && s.charAt(j) != s.charAt(i)) { j = result[j - 1]; } if(s.charAt(j) == s.charAt(i)) { j++; } result[i] = j; } return result; }
现在,我们也已经明白为什么要费这么大的劲来算一个这样的结果数组。这个结果数组表示的是对应匹配到某个长度字串时它们的前后缀覆盖长度,也表示我们在模式串里进行下一个比较的索引。
总过程实现
我们在前面的基础上实现完整的过程。这个过程的实现代码如下:
public static void kmp(String target, String pat) { int sourceLength = pat.length(); int targetLength = target.length(); int[] result = computePrefix(pat); int j = 0; for(int i = 0; i < targetLength; i++) { while(j > 0 && pat.charAt(j) != target.charAt(i)) { j = result[j-1]; } if(pat.charAt(j) == target.charAt(i)) { j++; } if(j == sourceLength) { System.out.println("find at index " + (i - j + 1)); j = result[j-1]; } } }
它的过程和前面的依次遍历的一个区别在于每次我们在碰到一个不匹配的时候,就通过pat字符串的匹配表往后退。和前面的比起来,它不需要两个串都退回到开始。由于在循环里我们可能查找到多个匹配的结果。我们在后面把每次匹配的索引都打印出来了。
所有完整的代码都放在附件里。
总结
前面对KMP算法的过程和推导讨论了这么多。这个问题的本质上还是对所有部分匹配的情况建立一个前后缀覆盖的关系表。以后查询的时候可以通过这个表来决定匹配多少个字符的时候从哪个字符开始进行匹配。这样减少了目标字符串的回退,使得算法的性能得到比较大的提升。其中的一个难点在于怎么建立这么一个匹配关系表。其实除了我们目前这种推导和建立匹配关系的方法。我们还有一种和有限状态机相关的方法来解决这个问题。在后续的文章里会对这一块做一个进一步的补充。
相关推荐
KMP算法的核心是构建一个“部分匹配表”(也称为“next值”),这个表记录了模式串在部分匹配情况下的下一步匹配策略。 1. **部分匹配表(next数组)**: next数组是KMP算法的关键,它表示当模式串的某个字符与...
### 数据结果 KMP算法实验报告 #### 实验背景与目的 本实验主要针对《数据结构》课程中的字符串处理部分,具体涉及的是模式匹配算法——KMP算法。通过实验加深学生对串类型及其基本操作的理解,并重点掌握两种重要...
本文将对这两种算法进行详细的分析和比较,以便更好地理解它们的原理和应用。 BF 算法 BF 算法(Brute Force Algorithm)是一种简单的字符串匹配算法,其主要思想是将目标字符串与源字符串进行逐个比较,直到找到...
KMP算法是通过分析子串,预先计算每个位置发生不匹配的时候,所需GOTO的下一个比较位置,整理出来一个next数组,然后在上面的算法中使用。
KMP算法,全称为Knuth-Morris-...总的来说,KMP算法及其改进版本在文本处理、搜索引擎、数据分析等领域有广泛应用。通过深入理解算法原理,结合实际应用场景选择合适的算法变体,可以有效地提高程序的效率和实用性。
程序的设计和实现过程中,我们详细介绍了数据结构、算法设计、程序实现等知识点,并且对KMP算法的实现过程进行了详细分析。 模式匹配的KMP算法是计算机科学领域中的一种经典算法,具有广泛的应用前景,本课程设计...
理解并掌握易语言KMP算法模块对于进行文本处理、数据分析以及日志分析等任务非常有帮助。它可以帮助你更有效地在大量文本中查找特定模式,提升程序的运行效率。同时,KMP算法也是字符串匹配领域的一个经典算法,学习...
《算法分析与设计:KMP算法的字符串匹配优化》 KMP算法,全称为Knuth-Morris-Pratt算法,是计算机科学中一种用于字符串匹配的高效算法。它避免了在进行比较时出现的不必要的回溯,从而显著提高了匹配效率。在给定的...
通过以上分析,我们不难看出KMP算法在字符串匹配中的高效性和实用性,尤其是在处理大量文本数据时,其优势更为明显。对于初学者而言,深入理解KMP算法的工作原理并掌握其实现细节,不仅能够提升编程技能,还能在实际...
以主串`A = "abababaababacb"`和模式串`B = "ababacb"`为例,我们来逐步分析KMP算法的具体执行过程。 1. **初始化**: `i = 0`, `j = 0`。 2. **逐字符匹配**: - 当`i = 1`时,`A[1] = a`,`B[1] = a`,`i++`,`j++...
模式匹配 KMP 算法 模式匹配 KMP 算法是一种高效的字符串匹配算法,由 D.E.Knuth、J.H.Morris 和 V.R.Pratt 同时发现。该算法可以解决模式匹配的问题,即在一个大字符串中查找一个模式字符串的出现位置。 模式匹配...
### 完全掌握KMP算法思想 #### KMP算法概览 KMP算法,全称为Knuth-Morris-Pratt算法,是一种高效的字符串匹配算法,主要用于在一个文本串中寻找一个模式串的所有出现位置。相较于朴素的字符串匹配算法,KMP算法...
本文将详细讨论KMP算法的原理,并着重探讨其在图形界面中的实现。 KMP算法的核心在于构造一个部分匹配表(也称为失配表),它记录了模式串中每个字符之前的所有前缀和后缀的最大公共长度。通过这个表,KMP算法可以...
`KMP算法.cpp`文件应该包含了这些实现逻辑,通过编译生成的`KMP算法.exe`可执行文件可以直接运行并测试。 4. **性能分析** KMP算法的时间复杂度为O(n + m),其中n为主串长度,m为子串长度。这是因为即使最坏情况下...
我们使用的教材是清华大学严蔚敏教授编写的《数据结构(C语言版)》,该教材难度较大,其实验方法特别是ADT方法在教材中介绍较少,KMP算法更是从理论分析的角度介绍了匹配算法和next的计算,自学难度很大。...
数据结构是计算机科学中的核心概念,它涉及到如何高效地存储和...学习KMP算法有助于提升解决相关问题的能力,如文本分析、文件查找等。通过深入研究压缩包中的代码和文档,可以进一步提升对KMP算法的理解和应用技巧。
《文档助手算法以及KMP算法分析》 在信息技术领域,高效的数据处理和文本搜索是至关重要的,这往往依赖于各种高级算法。KMP(Knuth-Morris-Pratt)算法,一种字符串匹配算法,因其高效的性能在文档助手类应用中得到...
KMP算法和Trie搜索树是两种在计算机科学中用于字符串处理的重要算法,它们在文本检索、模式匹配和数据结构优化等领域有着广泛的应用。 首先,我们来深入了解一下KMP算法。KMP(Knuth-Morris-Pratt)算法是一种高效...
KMP算法是字符串匹配问题的一个高效解决方案,通过部分匹配表减少了不必要的回溯操作,实现了线性时间复杂度的字符串匹配。理解部分匹配表的构建和利用是掌握KMP算法的关键。在实际编程中,KMP算法可以帮助我们更...