最近正在看《大话数据结构》,在看到KMP算法的时候怎么进行字符串匹配的基本上可以理解,但是在看源码求解next数组的时候,我蒙了,一头雾水,和我想象的完全不一样,真的好简洁,简洁到我都不知道为什么会这样写,我试图探究所以然,然后研究了将近三天时间,因此在这仅仅说明我对next数组的理解。如果理解错误的还请大佬指正,也欢迎交流,共同分享一下各自的想法。
KMP算法主要是用于解决字符串匹配的问题,算法的前世今生我就不说了,别的博客都有涉及到,而且不是今天文章的重点。KMP算法的基本思想就是“仅仅移动模式串,比较指针不回溯”,这是因为在朴素的字符串匹配算法中需要遍历主串和模式串,假设主串的长度为n,模式串的长度为m,因此朴素模式匹配算法的时间复杂度是O((n-m+1)*m),比较慢。在逐个匹配的过程中发现有很多不必要的额外比较操作,这些操作完全是可以通过改进算法进行规避的,因此KMP算法就这样问世了。它借助next数组,在不回溯主串比较指针的前提下,快速调整模式串从哪个元素的索引开始进行比较,因此从这两个方面上减少循环的次数,提高算法的速度。
首先,简单介绍KMP算法的处理流程,然后归纳出三种情况并通过代码实现。
假设模式串为“ABCABX”,下面数组的第一行表示各个字符的索引,第二行表示模式串中有六个字符。
0 | 1 | 2 | 3 | 4 | 5 |
A | B | C | A | B | X |
在了解KMP算法之前,还需要熟悉几个概念。第一个概念就是模式串中对称的“前缀”和“后缀”,举个例子,比如索引4也就是字符“B”与主串对应元素不相同,但是它之前的“ABCA”字符串中存在对称的“前缀”和“后缀”,就比如这样
我们就可以移动模式串的“前缀”到与之对称的“后缀”位置,就比如右图这样,这样就避免了多余的比较操作,否则就需要前移主串的比较指针并对模式串进行主串“A”与模式串“A”再一次进行比较的冗余操作。
因此,next数组中保存的就是当该字符与主串字符不匹配时,该字符应该回到哪个索引位置上再与主串进行比较。从上面的案例我们可以发现,当我们索引到字符B的时候,我们只需要观察字符B之前的“ABCA”字符串中是否存在对称前后缀即可,我们暂且用k和i表示前后缀尾索引,上述案例中,当求字符B的next数值的时候,因为“后缀”A和“前缀”A构成对称,因此,字符B的next数值就是“前缀”A的下一个索引位置,表示“前缀”和“后缀”进行对齐,主串和“前缀”A的下一个字符进行比较,如下图所示。
从上面的过程中,我们可以归纳以下几点:首先,“ABCABX”一共有6个字符,后缀尾索引的范围就是[0, 4];k表示上一个前缀尾索引的下一个字符的索引;next数组中存放着每个字符的回溯索引位置,而next数组中的值就与k值有着密不可分的关系。下面就来说明一下next数组如何确定。
下面归纳一下在比较的过程中会出现的三种情况:
(1)模式串当前字符之前的所有字符不存在对称的前后缀。这时候就需要移动主串比较指针,并设置该字符的next数组的数值为0,表示要回到模式串的首个位置处与主串进行比较。
(2)模式串当前字符之前的所有字符存在对称的前后缀。这时候就需要设置当前字符的next数组为前缀尾索引的下一个索引。
上面的两种情况,我们可以使用k和i通过比较来表示。
(1)如果k = -1或者T[k] == T[i],前者表明当前字符的next数组为k+1,并移动前缀尾指针的位置,我们可以用++k来代替上述两个步骤,然后就需要进行模式串中下一个字符next数组的计算,需要++i;T[k] == T[i]表明存在对称的前后缀,因此需要设置当前字符next的数组值为k+1,并移动i和k;因此上述两种情况都可以用next[++i] = ++k一行代码来表示。
(2)如果不存在对称的前后缀,我们就需要找第k个字符需要回溯的索引位置。为什么要回溯k的值,这是因为如果T[i] != T[k],那么就无法在上一个对称的前后缀的基础上进一步扩大对称前后缀的范围,因此只能在往前寻找能构成对称的前后缀。因为T[k-1]与T[i-1]相等,也就是说T[next[k]-1]和T[i-1]相等,这已经构成了一个对称前后缀,只需要判断T[next[k]]与T[i]是否相等,如果相等的话,就构成了一个最大的对称前后缀,否则再往前找。
因此上面的两种情况就可以用代码来表示:
void get_next(string T, int *next) {
int k = -1, i = 0;
next[0] = -1;
while (i < T.length()-1) {
if (k == -1 || T[i] == T[k])
next[++i] = ++k;
else
k = next[k];
}
}
因此,整个KMP算法代码就是这样的:
int index_KMP() {
const int SLEN = 15;
const int TLEN = 6;
char S[SLEN + 1] = "abcabcabcabcabx";
char T[TLEN + 1] = "abcabx";
int next[255]{ 0 };
get_next(T, next);
int i = 0;
int j = 0;
while (i < 15 && j < 6) {
if (S[i] == T[j]) {
i++;
j++;
}
else
// 仅仅移动模式串,比较指针不回溯
j = next[j];
}
if (j >= 6)
return i - 6;
else
return -1;
}