KMP算法
KMP算法是用来解决字符串匹配的问题。
*如:求文本串aabaabaaf中是否出现过模式串aabaaf。设遍历文本串时间复杂度为O(n),遍历模式串时间复杂度为O(m)。*
- 显然,暴力法求解是两层for循环,时间复杂度为O(m × n)。
- 下面介绍的KMP算法时间复杂度将会大大提高,为O(m + n)。
1.前缀表
①前缀和后缀
- 前缀:包含首字母,不包含尾字母的所有子串。
- 后缀:包含尾字母,不包含首字母的所有字串。
如:aabaaf
前缀包括:a、 aa、 aab、aaba、 aabaa五个子串。
后缀包括:f、 af、 aaf、 baaf、 abaaf五个子串。
②最长相等前后缀
如:
字符串a 最长相等前后缀为0
字符串aa 最长相等前后缀为1
字符串aab 最长相等前后缀为0
字符串aaba 最长相等前后缀为1
字符串aabaa 最长相等前后缀为2
字符串aabaaf 最长相等前后缀为0
③前缀表
前缀表就是记录字符串下标i之前(包括i)的字符串中,有多大长度的相同前缀后缀。
前缀表是用来回退的,它记录了模式串与主串不匹配的时候,模式串应该从哪里开始重新匹配。 即,当某个字符匹配失败时,前缀表告诉你下一步匹配时,模式串应该跳到哪个位置。
例:{0, 1, 0, 1, 2, 0}就是字符串aabaaf的前缀表。
④使用前缀表匹配的过程
如:文本串为aabaacaabaaf, 模式串为aabaaf。
-
文本串的c和模式串的f字符出现不匹配。我们要寻找模式串不匹配位置前面的子串的最长相等前后缀是2,模式串重新回到下标为2的位置开始匹配(2代表相等前后缀的长度,我们要跳到前缀的后面位置开始重新与文本串的字符进行匹配,而这个位置的下标正好等于前缀的长度)。
-
文本串c和模式串的b字符也出现不匹配。同上,寻找模式串不匹配位置前面的子串的最长相等前后缀是1,模式串重新回到下标为1的位置开始匹配。
-
文本串c和模式串的字符a也出现不匹配。寻找模式串不匹配位置前面的子串的最长相等前后缀是0,即从头开始匹配。
-
文本串c和模式串的首字符a也不匹配,文本串继续后移,找到了匹配的字符。
2.代码实现
①next数组
next数组是前缀表的代码实现,不涉及KMP的原理,next数组一般有2种表示方法:
以文本串aabaabaaf,模式串aabaaf为例。
- 前缀表作为next数组。 当遇见冲突时,返回到冲突前一位前缀表对应的值所对应的下标。
aabaaf的next数组为:[0 1 0 1 2 0],模式串最后一个元素f匹配失败,则模式串返回f前一位前缀表所对应的值即2所对应的下标,重新开始匹配。
- 前缀表整体右移一位,第一个元素赋值为-1。 遇见冲突时,直接返回next数组值对应的小标。
aabaaf的next数组为:[-1 0 1 0 1 2],模式串最后一个元素f匹配失败,模式串返回f所在next值即2所对应的下标,重新开始匹配。
②next数组实现代码(前缀表直接作为next数组)
求next数组的核心思想是——模式串与自身做匹配。 我们定义指针i和j。
- 指针i:指向后缀末尾位置
- 指针j:指向前缀末尾位置,也代表i之前的子串的最长相等前后缀长度。
求next数组的代码如下:
void getNext(int *next, const string &s) {
int j = 0;
next[0] = 0;
for (int i = 1; i < s.size(); ++i) {
//前缀末尾和后缀末尾不匹配的情况
while (j > 0 && s[i] != s[j]) {
j = next[j - 1];
}
if (s[i] == s[j]) {
++j;
}
next[i] = j;
}
}