KMP算法
简单模式匹配算法
简单模式匹配算法(俗称,暴力匹配算法)如下动图所示:
即S每一个子串都会与模式串P匹配一遍,其最坏时间复杂度为O(nm),n和m分别为主串和模式串的长度。
KMP 算法的核心思想
假设主串S和模式串P在第三个位置发生不匹配,这个时候我们其实可以确认前面一定是匹配的!!
即,如下图所示:
如果我们充分利用模式串的特征,下次匹配我们就可以保持主串不动,即只用挪动模式串,如下图所示:
从而达到减少匹配次数的效果。
因此,KMP算法的核心就是在分析我们的模式串,即next数组!
字符串的前缀、后缀和部分匹配值
- 前缀:除最后一个字符以外,字符串的所有头部子串;
- 后缀:除第一个字符外,字符串的所有尾部子串;
- 部分匹配值:字符串的前缀和后缀的最长相等前后缀长度。
图解获取 Next 数组代码
Next 数组的定义
对于字符串s的第i个字符s[i],next[i]定义为字符s[i]前面最多有多少个连续的字符和字符串s从初始位置开始的字符匹配。
即,当主串(i)与模式串(j)发生不匹配时,此时 j = next[j] 继续匹配(详见上面的KMP的核心思想)。
详解 Next 数组与部分匹配值之间的关系
如果主串和模式串在 i = 4 的时候,发生的不匹配,那么说明 i = 4之前的模式串是与主串匹配的,即如下图所示:
这个时候我们应该观察i = 4之前的子串的特征,而用之前的部分匹配值,我们可以得到’aba’子串的部分匹配值是1。
最后可得next[4] =1+1,即接下来,主串接着与模式串j = 2的位置继续匹配。
因此,最后的效果如下图所示:
详解 Next 数组
注意这里模式串a b a b a对应的下标分别为1 2 3 4 5
而 next 数组中存储的是 “模式串” 回溯时对应的下标
- 默认next[1] = 0;
- 通过前面对于 Next 数组和部分匹配值的关系,可以得知‘a’的最长相等前后缀长度为0,则next[2] = 0 + 1 = 1(next[2] 的值默认是 1)
- 求 next[2]:
(即,“ababa” 第三个值 “a” 匹配失败时应该 j 应该回溯的下标,因此我们需要考虑的是 “ab” 子串)
已知 next[1] 是在匹配第二个值 “b” 的时发生不匹配需要回溯的下标,而此刻我们在求 next[2] 的时候,是不是默认这个 “b” 匹配成功呢?
因此,只需要判断 next[1] 回溯时指向的元素和 “b” 是否相同,有以下两种情况:(假设 i 为所求 next 元素前一个元素,令 j = next[i],那么 next[i+1] 就是我们所要去求的值)
(1)如果 i == j;
(2)相同,则 next[i+1] = j + 1,结束;
(3)不相同,则 j = next[j],返回第一步继续比较。
这里理解比较困难,需要仔细想想:next 数组和部分匹配值之间的关系,以及给子串添加一个字符。
获取 Next 数组的代码(Java)
// 在前面的分析中,模式串下标是从 1 开始
// 而计算机存储中,串的下标都是从 0 开始的,故这里的 next 数组是分析的结果 -1
// next 中存的是“匹配”失败后,回溯后模式串的下标
public static int[] getNext(char[] s) {
// 开始构建 kmp 的 next 数组
int[] next = new int[s.length];
int i = 0, j = -1;
next[0] = -1;
while (i < s.length - 1) {
if (j == -1 || s[i] == s[j]) {
next[++i] = ++j;
} else {
j = next[j]; // 回溯
}
}
return next;
}
KMP 算法的优化 —— Nextval 数组
图解 Nextval 数组的核心思想
通过前面的分析,对于这样一个模式串,可以求的其 next 数组为 0 1 1 2 3 4。
那么请思考以下这个场景:在某一次匹配中,在 i = 4 的匹配失败,即如下图所示:
如果根据 next 数组的回溯规则进行回溯,那么就会变成以下的情况:
那么请思考,这一次比较的结果有什么意义吗?
总结:我们既然在 i = 4 的时候与 “b” 匹配发生了失败,是不是就可以确认主串中对应的值必定不为 “b”,因此我们可以根据这个特征对 next 数组进行改造,即为 nextval 数组。
详解 Nextval 数组
具体的步骤和求 next 数组差不多,只是多了一个当前字符和 next 数组对应值的比较的步骤
获取 Nextval 数组的代码(Java)
// 在前面的分析中,模式串下标是从 1 开始
// 而计算机存储中,串的下标都是从 0 开始的,故这里的 next 数组是分析的结果 -1
// nextval 中存的是“匹配”失败后,回溯后模式串的下标
public static int[] getNextval(char[] s) {
// 开始构建 kmp 的 next 数组
int[] nextval = new int[s.length];
int i = 0, j = -1;
nextval[0] = -1;
while (i < s.length - 1) {
if (j == -1 || s[i] == s[j]) {
++i;
++j;
if (s[i] != s[j]) {
nextval[i] = j;
} else {
nextval[i] = nextval[j];
}
} else {
j = nextval[j]; // 回溯
}
}
return nextval;
}