微信公众号:编程笔记本
点击上方蓝字关注我,我们一起学编程
欢迎小伙伴们分享、转载、私信、赞赏
昨晚逛 CSDN 的时候,偶然间看到了 KMP 算法的一篇文章,就想起了之前在学习算法与数据结构时留下的未解之谜,所以就决定把这个问题给它解决掉,彻底弄懂。
现在我就将我所理解的 KMP 算法分享给大家,且听!
- Brute-Force 算法
- KMP 算法
- 改进 KMP 算法
Brute-Force 算法
要引入 KMP 算法,我们还得从 Brute-Force 算法开始说起。
Brute-Force 算法,又称 BF 算法,中文翻译过来就是穷举、硬算的意思。是一种简单匹配算法,其基本思路如下:
从目标串 s="s0s1…sn-1"
的第一个字符开始,与模式串 t="t0t1…tm-1"
中的第一个字符比较,若相等,则继续逐个比较后续字符;否则从目标串 s
的第二个字符开始重新与模式串 t
的第一个字符进行比较。依此类推,若从模式串 s
的第 i 个字符开始,每个字符依次和目标串 t
中的对应字符相等,则匹配成功,该算法返回 i ,即目标串中字串的首字符下标;否则,匹配失败,函数返回 -1 。
匹配过程如下动图所示:
图片
这个算法很简单,思路也很清晰,这里我直接贴出参考代码:
int index(char s[], int sLen, char p[], int pLen)
{
int i = 0, j = 0;
if (sLen < pLen)
{
return -1;
}
while (i < sLen && j < pLen)
{
if (s[i] == p[i]) // 继续匹配下一个字符
{
++i;
++j;
}
else // 重新开始下一次匹配
{
i = i - j + 1; // 目标串从下一个位置开始
j = 0; // 模式串从头开始
}
}
if (j == pLen) // 模式串全部位于目标串当中
{
return i - pLen;
}
else
{
return 0;
}
}
这个算法很简单,易于理解,但效率不高,主要原因是目标串指针 i 在若干个字符序列比较相等后,若有一个字符比较不相等,仍需回溯,即 i=i-j+1
。BF 算法在最好情况下的时间复杂度为 O(m)
,即目标串的前 m 个字符正好等于模式串的 m 个字符。在最坏情况下的时间复杂度为 O(n*m)
,即模式串依次与目标串的所有字符进行比较。
KMP 算法
KMP 算法是 D.E.Knuth、J.H.Morris 和 V.R.Pratt 共同提出的,简称 KMP 算法。该算法较 BF 算法有较大改进,主要是消除了目标串指针的回溯,从而使算法效率有了某种程度的提高。
让我们先来看一个例子:
图片
现在我们来想一想,在第二次匹配的时候,真的有必要从 s[1]
开始吗?
既然 s[0]==p[0], s[1]==p[1], s[2]=p[2]
,那么我们何不从 s[3]
开始呢?由于我们在 s[3]
处失配了,现在又能在 s[3]
处开始新的匹配,这说明了什么?这不就是目标串指针是只进不退的嘛!也就是说,目标串指针不需要回溯。
那么,我们怎么知道该从何处开始新的匹配呢?
这就需要引入一个指引,指引我们从失配处寻找新的位置重新开始匹配。业界常将其称作 next[j]
函数。
下面的重点就是如何求出这个 next 函数。
所谓的 next 函数,其实本质上就是在模式串第 j 个索引处失配了,此前 j-1 个字符有没有首尾相同的子串。如果有,我们下一次可以跳过这些首尾相同的字串。这其中的原理是什么呢?试想一下,若这 j-1 个字符中有首尾相同的字串,那么目标串中这 j-1 个字符也同样有首尾相同的字串,那么我们就可以将模式串的尾串与目标串的首串直接匹配。
下面我们就来计算一下模式串的 next 函数。
图片
上述过程可以用如下代码表示:
void GetNext(char p[], int pLen, int next[])
{
int j = 0, k = -1;
next[0] = -1;
while (j < pLen - 1)
{
if (k == -1 || t.data[j] == t.data[k])
{
++j;
++k;
next[j] = k;
}
else
{
k = next[k];
}
}
}
有了 next 函数的辅助,现在很容易就得出 KMP 算法啦!
int KMPIndex(char s[], int sLen, char p[], int pLen, int next[])
{
int i = 0, j = 0;
while (i < sLen && j < pLen)
{
if (j == -1 || s[i] == p[j])
{
++i;
++j;
}
else
{
j = next[j]; // 目标串指针不变,模式串倒退
}
}
if (j == pLen)
{
return i - pLen; // 返回匹配模式串的首字符下标
}
else
{
return -1; // 返回不匹配标志
}
}
下面我们来看一下,用这个 KMP 算法来进行模式匹配的过程:
图片
我们惊奇地发现,废了这么半天劲折腾出来的 KMP 算法竟然没啥用?
哈哈,这就是后面我们需要再改进的地方。当然了,这只是一类情况,针对很多其他情况,现在的 KMP 算法已经很高效了。看一个例子:
图片
可以看到,我们在第二次就匹配成功了,而 BF 算法需要到第三次才会匹配成功。
设目标串 s 的长度为 n ,模式串 p 长度为 m 。在 KMP 算法中求 next 数组的时间复杂度为O(m) ,在后面的匹配中因目标串 s 的下标不减即不回溯,比较次数可记为 n ,所以 KMP 算法总的时间复杂度为 O(n+m)
。
下面我们针对上面的不足之处做一下改进。
主要就是修改 next 函数:
void GetNext(char p[], int pLen, int next[])
{
int j = 0, k = -1;
next[0] = -1;
while (j < pLen)
{
if (k == -1 || p[j] == p[k])
{
++j;
++k;
if (p[j] != p[k])
{
next[j] = k;
}
else
{
next[j] = next[k];
}
}
else
{
k = next[k];
}
}
}
此时,KMP 算法已经完整。
这节内容整理得不是很好,初次接触 KMP 算法的小伙伴应该是读不懂的哈哈。以后有好的讲解思路我再分享过来,今天先到这。
散会!