(原创文章,转载请说明出处)
KMP算法是由Knuth,Morris和Pratt三人设计的线性时间字符串匹配算法。其以的时间界执行模式匹配,其中
为目标串长度。在匹配过程中,KMP算法需要用到辅助函数
,而它也仅仅需要
时间来根据模式串计算出来,其中
为模式串长度。
算法思想
KMP模式匹配算法之所以能够快速进行模式匹配,是因为其在模式匹配过程中利用模式串自身的信息去规避了无用检测,并因此不需要在失配时进行如朴素模式匹配算法中的大量的回溯操作。它结合了模式串自身的信息,并在失配时结合已经成功匹配的前缀,并将模式串向前移动并避开重复比较的步骤。
算法原理
匹配过程
首先观察下面的KMP算法模式的匹配过程:
其中为目标串,
为模式串,
为目前匹配位置的起始,
为成功匹配的前缀长度。可见,阴影部分为已经成功匹配的前缀,而在该前缀后一位目标串上的
与模式串的
失配。此时,KMP算法会根据已经匹配的长度为
的前缀的最长相同前后缀的长度来决定模式串应当向前移动多少位。就上图中的例子而言,已经匹配的部分
的真前缀(不包含自身的前缀)为
,而它的真后缀为
,可见其最长的相同前后缀为
,其长度为
。此时KMP算法会要求下一次匹配从模式串已匹配前缀的最长相同前后缀的长度处的下一位开始匹配,这里即从
位置
上的
开始继续匹配,如下图:
随后,KMP算法又从之前失配的位置重新开始尝试匹配。值得一提的是,这种将模式串向前移动的操作恰好避开重复检测并能使得模式串向前移动最长的距离。在上面的例子以及普遍的情况下,若前移的距离比正确距离短,则会导致新的位置下的模式串失配(移动距离更短意味着这个前缀有着更长的相同前后缀),若前移的距离比正确距离长,则会导致算法缺失匹配步骤,甚至错过正确的解。因此,通过这样的匹配过程,KMP模式匹配算法能够正确而快速地找到目标串中模式串的位置。
前缀函数
上面关于KMP算法模式匹配过程的描述中提到了一个关键辅助参数——已匹配前缀的最长相同前后缀长度。在模式匹配过程中,失配可能从模式串的任意位置发生,所以也需要获得任意长度前缀的最长相同前后缀长度。而前缀函数,便是一个要获得任意长度前缀的最长相同前后缀长度的过程。
(to be continued)
C++实现
下面的函数是KMP算法的C++实现,因为目标串中可能包含多个与模式串相同的子串,所以该函数可能会返回多个成功匹配的首位置。这里用变长数组实现了该功能,并且当没有在目标串中找到模式串时会返回空的数组。
#include <vector>
#include <string>
using namespace std;
vector<int> & KMPmatch(string & obj, string & pattern);
int * prefix(string & pattern);
#include "match.hpp"
vector<int> & KMPmatch(string & obj, string & pattern)
{
//Args:
// obj : object string
// pattern : pattern string
//Returns:
// Start position of matched substring
// Initialize
auto n = obj.length(), m = pattern.length();
int p = 0;
int * pref = prefix(pattern);
auto pos = new vector<int>;
for (int i = 0; i < n; ++i)
{
// Mismatch handling
while (p > 0 && pattern[p] != obj[i])
p = pref[p - 1];
// Matched
if (pattern[p] == obj[i])
++p;
// Pattern matched, looking for the next match
if (p == m)
{
pos->insert(pos->end(), i - m + 1);
p = pref[p - 1];
}
}
return *pos;
}
int * prefix(string & pattern)
{
//Args:
// pattern : pattern string
//Returns:
// Prefix array
auto m = pattern.length();
int k = 0;
int * pref = new int[m];
pref[0] = 0;
for (int p = 1; p < m; ++p)
{
// Mismatch handling
while (k > 0 && pattern[k] != pattern[p])
k = pref[k];
// Position k matched
if (pattern[k] == pattern[p])
++k;
pref[p] = k;
}
return pref;
}
正确性证明和算法分析
(to be continued)