什么是模式匹配
模式匹配(Pattern Matching)就是在一个长度为n的文本S中,找某个长度为m的关键字P。
KMP算法如何来的?
KMP算法是由朴素字符串匹配算法优化而来,就是重新利用了朴素做法中浪费的一些有用信息。
- 不妨我们可以思考一下朴素做法如何实现。
假设存在S = “abcde123”,P = “123”。
暴力实现模式匹配的方式:从S的第一个字符开始,逐个匹配P的每个字符。在第一轮的匹配中,P[0] != S[0],匹配失败,后面的P[1],P[2]就不用比较了。然后P向后挪一位继续匹配,直到P每位都匹配成功为止。
详细过程如下图:
这种做法形象的看成一个滑块,在轨道S上滑动,直到完全匹配。
- 时间复杂度
由于上面哪个例子比较特殊,P和S的字符基本上都不一样。由于在每一轮匹配中,往往第一个字符就匹配不上,用不着匹配后面,所以此时时间复杂度相对乐观,多不多为O(n)。这是暴力匹配能到的最优复杂度了。我们换一种情况,假设S= “aaaaaaaab”,P= “aab”。在这种情况下每次都是在匹配P的第三位字符出错,P移位后,P又从头开始匹配。这种时间复杂度就是O(n*m)。
3. 如何实现优化朴素做法
在朴素方法中,每次新的匹配都需要对比 S和 P的全部 m个字符,这实际上做了重复操作。例如第一轮匹配 S的前 3 个字符 “aaa” 和 P 的 “aab”,第二轮从 S 的第 2 个字符 ‘a’ 开始,与和 P 的第一个字符 ‘a’ 比较,这其实不必要,因为在第一轮比较时已经检查过这两个字符,知道它们相同。如果能记住每次的比较,用于指导下一次比较,使得 S 的 i 指针不用回溯,就能提高效率。
如何利用不回溯快速实现模式匹配
在KMP算法中,指向S串的i指针是不需要回溯的,只需要考虑指向P串的j指针的回溯即可。j回溯多少取决于S串中有多少能和P串匹配。
P串和S串出现不匹配之前有两种情况:
- P在不匹配之前的每个字符都不同
例如 S = “abcabcd”,P= “abcd”,第一次匹配的失配点是 i=3,j=3。失配点之前的 P 的每个字符都不同,P[0]≠P[1]≠P[2];而失配点之前的 S与 P 相同,即 P[0]=S[0]、P[1]=S[1]、P[2]=S[2]。下一步如果按朴素方法,j要回到位置 0,i 要回到 1,去比较 P[0] 和 S[1]。但 i 的回溯是不必要的。由 P[0]≠P[1]、P[1]=S[1] 推出 P[0]≠S[1],所以 i 没有必要回到位置 1。同理,P[0]≠S[2],i 也没有必要回溯到位置 2。所以 i 不用回溯,继续从 i=3、j=0 开始下一轮的匹配。
2. P在失配点之前的字符有部分相同
(1) 相同的部分是前缀和后缀
当 P 滑动到下面左图位置时,i和 j 所处的位置是失配点,j 之前的部分与 S 匹配,且子串 1(前缀)和子串 2(后缀)相同,设子串长度为 L。下一步把 P 滑到右图位置,让 P 的子串 1 和 S 的子串 2 对齐,此时 i 不变、j=L,然后开始下一轮的匹配。注意,前缀和后缀可以部分重合。
(2) 相同部分不是前缀和后缀
下面左图,PP滑动到失配点 i 和 j,前面的阴影部分是匹配的,且子串 1 和 2 相同,但是 1 不是前缀(或者 2 不是后缀),这种情况与“1. P 在失配点之前的每个字符都不同”类似,下一步滑动到右图位置,即 i 不变,j 回溯到 0。
KMP的核心ne数组计算
在ne数组中,ne[j]数组就是P[0]-P[j-1] 这部分子串的前缀和后缀的最长交集的长度(最长公共前后缀)。
ne数组计算原理:
int ne[N];
for (int i = 2, j = 0; i <= m; i ++ )
{
while (j && p[i] != p[j + 1]) j = ne[j];
if (p[i] == p[j + 1]) j ++ ;
ne[i] = j;
}
KMP模式匹配过程和求ne数组是一样的,只是换了两个不同的字符串进行操作。
KMP代码模板
求模式串的Next数组:
for (int i = 2, j = 0; i <= m; i ++ ){
while (j && p[i] != p[j + 1]) j = ne[j];
if (p[i] == p[j + 1]) j ++ ;
ne[i] = j;
}
// 匹配
for (int i = 1, j = 0; i <= n; i ++ )
{
while (j && s[i] != p[j + 1]) j = ne[j];
if (s[i] == p[j + 1]) j ++ ;
if (j == m)
{
j = ne[j];
// 匹配成功后的操作
}
}