引言
其实网上有很多讲解KMP算法的文章,详略不一,我认为有两点没有解释清楚:
第一点:匹配失败以后,模式串的位移
第二点:next数组的生成算法
希望本篇文章能将KMP算法清晰易懂的拆解开来。
暴力匹配
假如你是一名生物学家,现在,你的面前有两段 DNA 序列 S
和 T
,你需要判断 T
是否可以匹配成为 S
的子串:
我们很容易想到的一个方法就是对每个字符进行逐一比较。
首先:我们从左边第一个位置开始进行逐一比较:
这样,当匹配到 T
的最后一个字符时,发现不匹配,于是从 S
的第二个字符开始重新进行比较:
仍然不匹配,再次将 T
与 S
的第三个字符开始匹配......不断重复以上步骤,直到从 S 的第四个字符开始时,最终得出结论:S
与 T
是匹配的。
大家发现这个方法的弊端了吗?我们在进行每一轮匹配时,总是会重复对 A 进行比较。也就是说,对于 S 中的每个字符,我们都需要从 T 第一个位置重新开始比较,并且 S 前面的 A 越多,浪费的时间也就越多。假设 S 的长度为 m,T 的长度为 n,理论上讲,最坏情况下迭代 m−n+1 轮,每轮最多进行 n 次比对,一共比较了 (m−n+1)×n 次,当 m>>n 时,渐进时间复杂度为 O(mn)。
改进方案
我们再来看一个例子,现在有如下字符串 S
和 P
,判断 P
是否为 S
的子串:
我们仍然按照原来的方式进行比较,比较到 P
的末尾时,我们发现了不匹配的字符。
注意,按照原来的思路,我们下一步应将字符串 P 的开头,与字符串 S 的第二位 C 重新进行比较。那能不能改进一下呢?当 T 和 Y 不匹配时,我们仔细观察发现在字符 Y的前面有一个子串:“ACTGPAC”,该子串有相同的部分AC。当比较到S的T位置时,其实我们已经知道S 中的蓝色 AC 与P中右侧的 AC是匹配的,又因为P中左侧也有AC,如果我们将字符串 P 需要比较的位置重置到图中 j 的位置,S 保持 i 的位置不变,接下来即可从 i,j 位置继续进行比较,这样不就能大大简化匹配过程了吗?这就是 KMP 的核心思想。
备注:【上述部分内容来源:力扣(LeetCode)】
KMP算法
简介:
Knuth-Morris-Pratt 字符串查找算法,简称为 “KMP算法”,常用于在一个文本串S内查找一个模式串P 的出现位置,这个算法由Donald Knuth、Vaughan Pratt、James H. Morris三人于1977年联合发表,故取这3人的姓氏命名此算法。
它是一种改进的字符串匹配算法,它的核心是利用匹配失败后的信息,尽量减少模式串与主串的匹配次数以达到快速匹配的目的。它的时间复杂度是 O(m+n)。
算法流程:
- 假设现在文本串S匹配到 i 位置,模式串P匹配到 j 位置
- 如果j = -1,或者当前字符匹配成功(即S[i] == P[j]),都令i++,j++,继续匹配下一个字符;
- 如果j != -1,且当前字符匹配失败(即S[i] != P[j]),则令 i 不变,j = next[j]。此举意味着失配时,模式串P相对于文本串S向右移动了j - next [j] 位。
- next 数组存储的值的含义:代表当前字符之前的字符串中,有多大长度的相同前缀后缀。例如如果next [j] 等于 k,代表 j 之前的字符串中有最大长度为k 的相同前缀后缀。
最长前缀后缀:
前缀:
除去字符本身,它前面的字符能组成的所有组合,这里注意一下,必须以第一个字符开头,比如字符串abc,它的前缀有a,ab
后缀:
除去字符本身,它后面的字符能组成的所有组合,,这里注意一下,必须以最后一个字符开结尾,比如字符串abc,它的后缀有c,bc
前缀后缀的公共元素:这个就非常简单了,不再解释。
最大长度表【最大公共元素长度】:
如果给定的模式串是:“ABCDABD”,从左至右遍历整个模式串,其各个子串的前缀后缀分别如下表格所示:
取上表的第一列和最后一列,然后进行行列转换一下得到如下的《最大长度表》:
基于最长前缀后缀的匹配:
因为模式串中首尾可能会有重复的字符,故可得出下述结论:不匹配时,模式串向右移动的位数为:已匹配字符数 - 失配字符的上一位字符所对应字符串的最长前缀后缀
如果给定文本串“BBC ABCDAB ABCDABCDABDE”,和模式串“ABCDABD”,现在要拿模式串去跟文本串匹配,如下图所示:
1. 因为模式串中的字符A跟文本串中的字符B、B、C、空格一开始就不匹配,所以不必考虑结论,直接将模式串不断的右移一位即可,直到模式串中的字符A跟文本串的第5个字符A匹配成功:
2. 继续往后匹配,当模式串最后一个字符D跟文本串匹配时失配,显而易见,模式串需要向右移动。但向右移动多少位呢?因为此时已经匹配的字符数为6个(ABCDAB),然后根据《最大长度表》可得失配字符D的上一位字符B对应的长度值为2,所以根据之前的结论,可知需要向右移动6 - 2 = 4 位。
3. 模式串向右移动4位后,发现C处再度失配,因为此时已经匹配了2个字符(AB),且上一位字符B对应的最大长度值为0,所以向右移动:2 - 0 =2 位。
4. A与空格失配,向右移动1 位。
5. 继续比较,发现D与C 失配,故向右移动的位数为:已匹配的字符数6减去上一位字符B对应的最大长度2,即向右移动6 - 2 = 4 位。
6. 经历第5步后,发现匹配成功,过程结束。
备注:【上述原文链接:https://blog.csdn.net/v_july_v/article/details/7041827】
next数组:
由来:
通过上一节,我们可以看到,当我们要匹配失败的时候,需要去找匹配失败的字符的上一位字符对应的最长前缀后缀,那么如果我们将这个最大长度表的值整体右移一位,不就更简单了吗,这就是我们的next数组。next 数组就是求得了最大长度表,然后整体右移一位,对next[0]赋值为-1,也即:
算法实现:
前面我们通过一步一步的推导,求得了最大长度表,然后又将其右移一位得到next数组,那么我们如何通过代码来求得next数组呢?
对于P的前j+1个序列字符:
若p[k] == p[j],则next[j + 1 ] = next [j] + 1 = k + 1; -- 这个大家理解起来都没有问题
若p[k ] ≠ p[j],如果此时p[ next[k] ] == p[j ],则next[ j + 1 ] = next[k] + 1,否则继续递归前缀索引k = next[k],而后重复此过程。--这个理解起来就非常困难了,我们看下面的图:
1、p[k] != p[j]
2、p[0]到p[k-1] 等于 p[j-k]到p[j-1]的,也就是图中最上面K个元素
3、我们现在要找到新的p[0]到p[k’-1] 等于 p[j-k’]到p[j-1],那么这个新的k怎么求呢?
4、因为p[0]到p[k-1] 等于 p[j-k]到p[j-1],所以上图中蓝2色块与蓝4色块相等的,现在又要找蓝1色块+黄1色块 等于蓝4色块+黄2色块,所以蓝1色块等于蓝2色块,也意味着我们在找p[k]左边的字符串的最长前缀后缀,而这正是next[k],是不是无巧不成书。
算法代码:
next数组代码:
int[] getNext(String p) {int l = p.length();int[] next = new int[l];int j = 0;int k = -1;next[0] = -1;while (j < l -1) {if (k == -1 || p.charAt(j) == p.charAt(k)) {k++;j++;next[j] = k;} else {k = next[k];}}return next;}
KMP算法
int KmpSearch(char* s, char* p)
{int i = 0;int j = 0;int sLen = strlen(s);int pLen = strlen(p);while (i < sLen && j < pLen){ if (j == -1 || s[i] == p[j]){i++;j++;}else{j = next[j];}}if (j == pLen)return i - j;elsereturn -1;
}