官方定义
KMP算法是一种改进的字符串匹配算法,KMP算法的核心是利用匹配失败后的信息,尽量减少模式串与主串的匹配次数以达到快速匹配的目的明。
引入概念
前缀:字符串以首字符为开始,至除了最后一个字符为结束的任一子串称为前缀。如"ababa"的前缀可以是“a”,“ab”,“aba”,“abab”
后缀:字符串以除了首字符以外的任一字符为开始,至最后一个字符为结束的任一子串称为前缀。如"ababa"的后缀可以是“baba”,“aba”,“ba”,“b”
举个例子
在主串S = “babacababacababaaaba” 中找到模式串T = “ababaaaba” 的起始下标位置
按朴素模式匹配算法(这里不做赘述)需要将子串逐一向后移动一位做循环匹配,极端情况下主串S中有多少字符就要循环S.Length * T.Length 次,效率低下。
KMP模式匹配算法通过相同的前后缀不做重复比较做到对模式串T 的快速回溯,减少比较的次数,步骤如下:
1. 当 i = 1 , j = 1 时,S[i] = b ≠ a = T[j],i++ 模式串T向后移动一位继续匹配
2. 当 i = 2, j = 1 时,S[i] = b = a = T[j],i++, j++ 字符匹配,进入匹配下一字符
3. 当 i = 3, j = 2 时,S[i] = b = a = T[j],i++, j++ 字符匹配,进入匹配下一字符
4. 当 i = 4, j = 3 时,S[i] = b = a = T[j],i++, j++ 字符匹配,进入匹配下一字符
5. 这里有重点 ! 当 i = 5, j = 4 时,S[i] = b ≠ a = T[j],此时我们并不像朴素模式匹配那样将模式串T向后一个一个比较,由于之前的“aba”中 ‘a’已经有过给重复的比较,这里直接将T串后移两位,见下图
6. 此时 i = 5,j = 2,S[i] = b ≠ a = T[j],字符依旧不匹配,T串继续后移一位
7. 此时 i = 5,j = 1,S[i] = b ≠ a = T[j],这时候说明 i = 5 对于 j < 4 一定不存在匹配关系,模式串继续后移一位,i++
注:对于代码中的操作可以理解为T串后移一位,但 j = 0 不指向首字符,进入下一步匹配时 i++,j++
8. 当 i = 6, j = 1 时,S[i] = b = a = T[j],i++, j++ 字符匹配,进入匹配下一字符,接下来就是重复步骤2 - >步骤4做匹配至下一不匹配位置
9. 当 i = 11, j = 6 时,S[i] = b ≠ a = T[j],由于之前的“ababa”中 ‘aba’已经有过给重复的比较,所以我们这里直接后移两位,见下图
10. 当 i = 11, j = 4 时,S[i] = b ≠ a = T[j],依旧不匹配,接下来就是重复步骤5 ->步骤7,进入下一步
11. 当 i = 12, j = 1 时,S[i] = b = a = T[j],i++,j++,接下来重复步骤2 - >步骤4进行匹配至结束可以发现匹配成功
总结一下上述步骤
- 当不匹配时,主串S中 i 并不受影响,主要还是模式串T中的 j 在做变化,所以仅需对模式串中的 j 进行剖析
- 不匹配时,模式串T之前已经匹配过的字符串中有相等的前后缀可以不用再进行一次匹配
- 已匹配过的字符串存在前缀等于后缀,且为最长的前后缀,这就是可以直接跳过的匹配长度
如步骤5中 “aba” 最长相等前后缀为 ‘a’ ,T串后移两位,即 j = 4 回溯为 j = 2
步骤9中”ababa“最长相等前后缀为 ”aba“ ,T串后移两位,即 j = 6 回溯为 j = 4 - 通过1和3可以得出结论,当不匹配时,j 回溯至最长匹配前后缀长度加一的位置,可以理解为这个不合适,换下一个来,像步骤 5 中 j = 4 不匹配,需要将j回溯为 2 进行匹配。此时可以解释步骤6不匹配时而且无最长匹配前后缀为什么j = 2 -> j = 1, 因为没有所以长度为0,故为 j 回溯为 1
next数组推导
通过上述步骤我们得出结论,匹配时与i无关,按kmp算法的思想,我们需要针对T串提前找出每一位在不匹配时需要将j回溯的位置,就可以快速匹配子串,而每一位需要回溯的位置就存于next数组中(其实就是自己定义的一个数组,好多地方都这么命名)。
next数组公式(取自《大话数据结构》):
当 j = 1 时,第一位不匹配,代码里的实现是,j 回溯为0 进而 i++,j++,其实就是将 T串第一个字符与S串的下一个做比较,
Max 没太看懂,看其他资料作比较,大概就是T串中已匹配子串的最长重复前后缀长度加一
其他情况就是无重复前后缀,因为此时最长重复前后缀长度为0,加一就是1了
所以通过这个公式可以手算出任一串的next数组,如上述例子 ”ababaaaba“ 的next数组为 [011234223]
代码推导逻辑
next数字推导回溯位置其实就是将T串与自己进行错位匹配,如下
1.默认第一位不匹配时回溯为 0,即next[1] = 0,代表T串已无法再回溯,需要与S串下一位进行比较,初始设定 i = 1, j = 0,表示 i 指向第一位,j 还没有指向,此时说明当前子串为 “a” 无重复前后缀,所以这里记作在下一位即第 2 位上不匹配时无重复前后缀就回溯为 1,即 next[++i] = ++j,同时 i 和 j 指向下一位
2.此时当前子串为 “ab” ,显然 t[i - 1] ≠ t[j - 1] 表示再 j = 1 时T串第一位不匹配需要回溯,开始已经默认第一位回溯位置为0,即next[1] = 0,所以这里 j 回溯为 0
3. 这时候出现了和步骤一样无法比较的情况,所以此时表示子串 “ab” 无重复前后缀,所以这里记作在下一位即第 3 位上不匹配时无重复前后缀就回溯为 1,即 next[++i] = ++j,同时 i 和 j 指向下一位
4. i = 3, j = 1, 这时候 t[i - 1] = t[j - 1],说明 i <= 3时,子串"aba"有最长重复前后缀"a",所以这里记作在下一位即第 4 位上不匹配时有重复前后缀长度为 1 ,回溯位置为 2,即 next[++i] = ++j,同时 i 和 j 指向下一位
5.接下来逻辑同步骤4,不做赘述,得出next[5] = 3, next[6] = 4,至下一个不匹配情况
6. 当 i = 6,j = 4 时,t[i - 1] ≠ t[j - 1],我们需要对 j 在第 4 位不匹配时进行回溯,由于之前已经得出next[4] = 2,所以这里对 j 进行回溯至 2
7. 此时 i = 6,j = 2,t[i - 1] ≠ t[j - 1],依旧不相等,j 继续回溯至next[2] = 1 的位置
8. 此时 i = 6,j = 1,t[i - 1] = t[j - 1],二者相等,终于得出下一位即第 7 位上不匹配时有重复前后缀长度为 1 ,回溯位置为 2,即 next[++i] = ++j,同时 i 和 j 指向下一位,接下来就是重复上述步骤至 i 结束,得出T串的next数组为 [011234223]
代码实现:
/// <summary>
/// 字串每个位置不匹配后需回溯的位置
/// ”ababaaaba“执行结果为”0011234223“
/// </summary>
/// <param name="t"></param>
/// <returns></returns>
public int[] GetNext(string t)
{
//因为是从数组下标为1开始记录的,所以数组长度要T串长度加一,初始化后next[1]默认为0
int[] next = new int[t.Length + 1];
int i = 1;
int j = 0;
while (i < t.Length)
{
//j=0表示j已无法回溯,若此时还没找到相等的前后缀代表没有了
//t[i - 1] == t[j - 1]表示当前有相等的前后缀,为下一位做回溯标志,同时 i 和 j 指向下一位作比较
if (j == 0 || t[i - 1] == t[j - 1])
next[++i] = ++j;
//条件不满足时,需要对j逐步回溯缩短前缀的长度做比较,由于已经默认了第一位回溯至0,所以到最后回溯至 j = 0 时就无法进行回溯,表示无相等前后缀
else
j = next[j];
}
return next;
}
每一位不相等需要回溯的位置next数组已经得到,接下来就是T串在S串的查找了
/// <summary>
/// 子串查找
/// </summary>
/// <param name="s"></param>
/// <param name="t"></param>
/// <param name="next"></param>
/// <returns></returns>
public int Index_KMP(string s, string t, int[] next)
{
//同样这里以数组下标加一做处理,初始化i,j为0表示无指向
int i = 0, j = 0;
while (i <= s.Length && j <= t.Length)
{
//j == 0 表示T串无指向,匹配第一位
//s[i - 1] == t[j - 1]表示当前位相等,匹配下一位
if (j == 0 || s[i - 1] == t[j - 1])
{
i++;
j++;
}
//条件不满足时,直接从next数组中找到对应位置将j回溯一下进行下一次比较即可
else
{
j = next[j];
}
}
//这里表示T已经匹配至最后,返回T串在S串中的初始位置,没有返回-1表示不存在。注意:这时候返回的就不是数组下标加一了
if (j >= t.Length)
{
return i - j;
}
else
{
return -1;
}
}
做个记录防止后面忘记了,希望下次看到能马上想起来吧。。。