目录
KMP算法引入:
在文本中查找一个模式串:KMP算法可以快速地在一个文本串中查找是否包含一个给定的模式串,时间复杂度为O(n+m)。
讲解样例(接下来的算法讲解都采用此样例)
主串:ABAABABABCA
模式串:ABABC
朴素算法(暴力匹配):
我们先来观察一下暴力算法的过程:
从主串的第一个字符开始与模式串的第一个字符比较,如果相同则模式串往后移动一个位置和主串的下一个位置再比较,如果不同,则主串移动到下一个字符,模式串移动到第一个字符,开始重新比较,以此类推。
这种方法逻辑简单,但效率很低,假设主串的长度为m,模式串的长度为n,最差的情况是主串移动m-n+1次才能匹配到,时间复杂度为O(mn),直接上代码:
int n = s.length(), m = p.length();
if (m > n) return -1; // 模式串不可能是主串的子串
for (int i = 0; i <= n-m; i++) {
int j = 0;
while (j < m && s[i+j] == p[j]) j++;
if (j == m) return i; // 匹配成功,返回匹配的位置
}
return -1; // 匹配失败
按照上面的逻辑运行一下我们的测试样例:
我们仔细观察一下,就会发现会有许多无效匹配。 例如在第一幅图中,匹配到S[i] != P[j],i 总结出下标为3的位置回退到了下标为1的位置,j直接回退到模式串的开头。但我们仔细观察一下,真的需要回退这么多步骤吗?
而模式串的绿色部分有一个特点:前缀和后缀存在相同的部分。"A"
而主串和模式串的绿色部分又是相同的部分,就代表着主串的绿色部分也有着相同的前后缀。既可认为A1 = A2 = A3 = A4.
在之前的匹配中我们已经将A2与A4匹配成功,而A3与A4是相同的前后缀,所以我们可以认为我已经将A2与A3匹配完成。因此我们就不需要移动 i 的下标,同时只需要将 j 的下标移动到1的位置也就是“B"的位置。我们就节省了从头匹配的过程,这也是KMP算法的精髓:储存模式串最长相同前后缀,通过最长前后缀信息来进行快速移动。
这样以来,大大节省了匹配速度,面对失配情况,我们可以通过模式串P对应位的最长公共前后缀的长度,进行快速移位。不再需要像朴素算法那样,进行i的回溯和j的归零。
KMP算法:
上面讲到KMP算法可以储存模式串最长相同前后缀的长度,通过最长前后缀信息来进行快速移动。而其中就是采用next数组来储存这部分的信息。
在KMP算法中,next数组的形式有两种:
- 从下标0开始,将整体右移一位,下标0的位置赋值为-1。
- 弃用下标0的位置,从下标1开始,整体减一。
这两种不同的方式会影响匹配函数的编写逻辑。本文重点以第一种情况展开讲解。如果你想要了解第二种的next数组只需要将第一种的每个数加一即可。
当然如果你是初学者,这些都不用烦扰。无论哪一种都可以解出正确的答案。目前可以专注一种方法。
(一) 构造next数组(情况一)
next数组的值计算的是从前面字符串到下标前一个位置的这段字符串的最大公共前后缀的长度,不包括整个字符串本身。
按照我们的例子ABABC
下标i = 0时,0前面没有字符串,所以我们统一初始化为-1
下标i = 1时,1前面字符串为A,不存在前后缀,所以next[1] = 0
下标i = 2时,2前面字符串为AB,没有相同的前后缀,next[2] = 0
下标i = 3时,3前面字符串为ABA,存在相同前后缀A,next[3] = 1
下标i = 4时,4前面字符串为ABAB,存在相同前后缀AB,next[4] = 2
下标i = 5时,5前面字符串为ABABC,不存在相同前后缀,next[5] = 0
按照上面的案例,我们应该开辟一个比模式串大1的空数组。将下标为0的位置初始化为-1,因为模式串第一个字符前面没有子串。初始化下标指针 i = 0,j = -1.
int i = 0, j = -1;
next[0] = -1;
那么下面就可以按照规定的步骤来得出next数组。我们将i命名为前进指针,j命名为长度指针。长度指针的大小就代表着匹配到的前后缀的长度
- 如果长度指针为-1,则代表为匹配到相同的前后缀,应该开始下一个的匹配。即让前进指针指向下一个,长度指针回复为0,next[前进指针] = 长度指针
- 如果前进指针和长度指针所指的字符相同,则将长度指针和前进指针加一,next[前进指针] = 长度指针
- 如果前进指针和长度指针所指的字符不相同,则将长度指针指向next[长度指针]
void KMPBuild(string patt, int len, vector<int>& next) {
int i = 0, j = -1;
next[0] = -1;
// 初始化next数组,第一个元素设为-1
while (i < len) {
if (j == -1 || patt[i] == patt[j])
// 当前字符匹配成功,继续向后匹配
next[++i] = ++j;
else
// 当前字符匹配失败,回退到上一个能够匹配的位置
j = next[j];
}
}
情况二简述:
第二种情况也是类似的道理,实现代码基本相同,只有初始化的细节区别。如果理解了第一种也应该可以理解第二种。这边我就只贴出相应的代码。
void KMPBuild(string patt, int len, vector<int>& next) {
int i = 0, j = 0;
next[1] = 0;
// 初始化next数组
while (i < len) {
if (j == 0 || patt[i] == patt[j])
// 当前字符匹配成功,继续向后匹配
next[++i] = ++j;
else
// 当前字符匹配失败,回退到上一个能够匹配的位置
j = next[j];
}
}
(二) 利用next数组来进行匹配
开始遍历文本串和模式串:
若不相同,模式串则需要从next数组中寻找下一个位置.
若相同,则i和j同时向后移动
当遍历模式串的j指针等于模式串的长度时即代表匹配成功。
void KMPBuild(string patt, int len, vector<int>& next) {
int i = 0, j = -1;
next[0] = -1;
// 初始化next数组,第一个元素设为-1
while (i < len) {
if (j == -1 || patt[i] == patt[j])
// 当前字符匹配成功,继续向后匹配
next[++i] = ++j;
else
// 当前字符匹配失败,回退到上一个能够匹配的位置
j = next[j];
}
}
int KMPSearch(string haystack, string needle) {
int n = haystack.size(), m = needle.size();
vector<int> next(m + 1, 0);
// 构建next数组
KMPBuild(needle, m, next);
int i = 0, j = 0;
// i用于遍历主串haystack,j用于遍历模式串needle
while (i < n) {
if (j == -1 || haystack[i] == needle[j]) {
// 当前字符匹配成功,继续向后匹配
++i;
++j;
} else {
// 当前字符匹配失败,回退到上一个能够匹配的位置
j = next[j];
}
if (j == m)
// 字符串needle已经完全匹配,返回匹配的起始位置
return i - j;
}
// 未找到匹配的子串,返回-1
return -1;
}
(三) KMP算法的优化
为什么KMP算法这么强大了还需要改进呢?
大家来看一个例子:
主串“aaaaabaaaaac”
模式串“aaaaac”
在这个例子中当‘b’与‘c’不匹配时应该‘b’与’c’前一位的‘a’比,是不匹配的。而'c’前的’a’回溯后的字符依然是‘a’。因为回退后的字符与回退前的字符是相同的,所以我们已经知道‘b’与‘a’相比的结果。原字符不匹配,回退后的字符自然也无法匹配。但在KMP算法中依然会将‘b’与回溯到的‘a’进行比对。这就是算法多余的部分,需要我们进行改进。
我们将改进后的数组命名为nextval数组。算法改进部分:
- 如果第i为字符与它next值指向的那位字符相同,为了避免无效的回退,则将该字符的nextval指向它next值指向的那位字符的nextval的值。
- 如果不相等,则该字符的nextval就是自己的next的值。
// KMP构建nextval数组
void KMPBuild(string patt, int len, vector<int>& nextval) {
int i = 0, j = -1;
nextval[0] = -1;
while (i < len) {
if (j == -1 || patt[i] == patt[j]) {
++i;
++j;
// 当前字符匹配成功,设置nextval值
if (patt[i] != patt[j])
nextval[i] = j; // 若下一个字符失配,则直接移动到j位置
else
nextval[i] = nextval[j]; // 若下一个字符也匹配成功,直接移动到nextval[j]位置
}
else {
j = nextval[j]; // 失配时根据nextval数组进行跳跃
}
}
}
// KMP搜索算法
int KMPSearch(string haystack, string needle) {
int n = haystack.size(), m = needle.size();
vector<int> nextval(m + 1, 0); // 创建nextval数组,长度为模式串长度+1
KMPBuild(needle, m, nextval); // 构建nextval数组
int i = 0, j = 0;
while (i < n) {
if (j == -1 || haystack[i] == needle[j]) { // 当前字符匹配成功
++i;
++j;
}
else {
j = nextval[j]; // 失配时根据nextval数组进行跳跃
}
if (j == m)
return i - j; // 匹配成功,返回开始匹配的位置
}
return -1; // 未找到匹配的子串
}