字符串匹配是算法中的一个重要领域,常用在计算机科学的自然语言处理和模式匹配中。
(一)经典的字符串匹配算法
(1)穷举或者暴力法/brute force简称BF。
(2)大名鼎鼎的KMP算法(Knuth-Morris-Pratt)。
(3)Horspool算法及Boyer-Moore算法。
(4)其他:如Sunday算法,BOM算法(Backward Oracle Matching),BNDM算法(Backward Nondeterministic Dawg Matching)等等。这些我根本就没有看过,本文中就不再讨论了。
(二)字符串匹配算法的联系和区别
我们要进行字符串匹配,肯定是一个字符一个字符的比较(一般是ASCII字符)。比较,就会出现匹配和不匹配两种情况。我们知道,如果匹配,则是我们希望看到的,那么可以继续比较下一对字符;但是我们不想看到的不匹配的情况却是不可避免的。
几乎所有字符串匹配算法的区别都在于:不匹配字符的处理。即当遇到不匹配的字符时,我们应该如何尽快的找到下一个匹配的状态或者说如何高效率的避免再次出现不匹配的状态。而除了穷举法之外,所有的字符串匹配算法都利用了另外的数据结构来解决不匹配字符的处理。因此也就是使用了时空权衡的思想(空间换时间),以储存多余的信息为代价提高算法效率。
这个多余的数据结构的意义就是:标记在遇到不匹配字符时,如何移动模式串以尽快的匹配目标串。(模式串和目标串是什么我就不说了。)
在不同的算法中名字往往不同(这是因为发现这些算法的人往往随便给自己的数据结构起名):KMP算法中,这个数据结构一般标记为next数组,有时候称为“跳转表”(jump table/shift table,我在下面会解释跳转是什么意思),有时候因为KMP本质上是一个自动机,这个数据结构也相应的称为“失败函数”或“失配函数”(failure);horspool算法中也需要这样一个跳转表;在Boyer-Moore算法中需要两个跳转表,分别是bad-character heuristics/good-suffix heuristics(坏字符启发和好后缀启发)。
关于horspool算法,可以参见这篇文章:字符串匹配之horspool算法。
关于Boyer-Moore算法,可以参见这篇文章:Boyer-Moore算法。
下面我仅以KMP算法为例(因为BM算法实在是够复杂),说明如何产生产生这个next数组,以及如何使用。
(三)KMP前言:暴力法/穷举法
要是想搞懂KMP,如果不搞懂最原始的暴力法字符串匹配,那KMP肯定是没戏的。
{
//目标串与模式串中的位置指针
int i = 0;
int j = 0;
while( i < target.size() && j < pattern.size() )
{
//如果相等则继续下一位的比较
if( target[i] == pattern[j] )
{
++i;++j;
}
//如果遇到失配,i、j都要回溯
else
{
//i回溯失配的长度+1
i = i-j+1;
//模式串回溯到首位
j = 0;
}
}
//模式串到尾部,说明全部匹配
if( j == pattern.size() )//++j
return i-j;
else
return -1;
}
例子就不举了。暴力法是典型的失败了就从头再来,小心谨慎的算法,也没有利用任何的多余信息,目标串是一位一位进行移动的,如果不成功则不仅模式串回溯,目标串也回溯的。在此仅举一个例子说明暴力法的低效,目标串为“aaaaaaaaaaab”,模式串为“aaab”,那么最坏的复杂度可以达到O(M*N),M和N分别是目标串和模式串的长度。
(四)KMP算法详解
关于KMP算法,网络上的资料真是汗牛充栋、良莠不齐,让人眼花缭乱,目不暇接。但是大部分都是摆代码,摆公式;最好的还是举例子,但是举完例子,跟代码又脱节了(网上流传的Matrix67那篇“KMP算法详解”就是如此),真正能帮助人好好理解这个算法的可真是大海捞针了。其实这个算法的确是不好理解,其次是理解的人各有各的理解方法,并不是放之四海皆准,而且理解了的人说出来的自己都不一定能看懂。。。我自己能力也有限,完全理解都不敢讲,但是希望能给那些跟我一样曾经迷茫于KMP的人以真正的帮助。
(4.1)构造跳转表next
一个原则先说明一下,但是不给出证明(可以参考相关资料):
跳转表next只与模式串的构造有关系,而且可以由模式串自匹配生成(用代码表示就是一个以模式串为单参数的函数)。
=========================================================
首先请原谅我创造了一个术语:等效前缀。这个词表示的是:从当前位置向前,与字符串从头开始,相重复的部分。
举个例子:p = "abcaabcab"中,设当前位置为p[6]=c(数组下标从0开始),那么c的等效前缀就是p[4...5]=ab:因为字符串从头开始,p[0...1]=p[4...5],此外找不到更长的重复部分,因此p[4...5]就称为等效前缀(真正的前缀是p[0...1])。又如"bcbcd"和"abcdabce"这样的字符串都包含有等效前缀。
其次解释一下什么叫做跳转。跳转的意思就是当发生失配的时候,模式串的移动。跳转表就是记录了模式串移动的位置。在某些地方,跳转与“回溯”是等同的。
=========================================================
下面介绍如何根据模式串的不同构造生成next数组。
(4.1.1)首先说明匹配过程中出现的比较容易处理比较容易理解的失配情况,例如模式串中的失配字符没有等效前缀的情况。
举例:设pattern = "abcab",现在我们看next[3]等于什么;即当"a"失配时,应该如何跳转。我们发现"a"是没有等效前缀的(不包括"a"本身):因为pattern[3]的"a"之前的"abc"中的子串"b"和"bc"都与以第一个"a"开始的同样长度的子串"a"和"ab"都不重复。所以当pattern[3]的"a"失配时,我们只能将"abca"全部移过失配位置(如下面的例子所示),所以next[3]=-1(-1表示匹配模式串的起始位置的更前一个位置,也就意味着目标串中的指针需要后移一位才能匹配模式串的第一个位置)。
a b c a b
"a"失配时,只能将整个模式串全部移过(没有别的可能,读者可以仔细想想为什么),如下:
a b c a b
(4.1.2)有人可能会问:如何检查一个字符串中有没有等效前缀呢?
这个仍然只跟模式串本身有关。我们使用一个含有等效前缀的串"abcabcd"做例子,那么当模式串进行自匹配时,是很容易发现等效前缀的:如果上下模式串的位置指针同时增长,就说明发现了等效前缀,这个编程也是容易实现的(在后面代码注释里有)。例如:
a b c a b c d
(4.1.3)下面要说的当然就是当失配时,模式串中有等效前缀的情况了。
b c a b c d
我们的模式传中例子中的"a"与"d"失配,但是我们下一次移动并不是将整个模式串移过目标串中的"a",很明显的,我们应该移动成下面的样子:
b c a b c d
这就是我们为什么将“等效前缀”称为“等效”的原因了:因为二者在匹配效果上是可以替代的,于是模式串中的前面的"bc"取代了后面"bc"的位置。注意:我们既没有浪费跳跃的机会,尽可能的跳过了更多的位置,又不会错过匹配的机会。
(4.1.4)下面就是完成整个模式串的自匹配过程,也就是构造next跳转表的代码。
{
//位置指针
int i = 0;
int j = -1;
//-1表示无法匹配,需要等待再次移位
next[0] = -1;
while( pattern[i] != '\0' )
{
// (a)j = -1意味着模式串的第一位与目标串第一位仍无法匹配,位置指针+1
// (b)后面一种情况表示匹配成功,则也需要位置指针+1
if( j == -1 || pattern[i] == pattern[j] )
{
++i; ++j;
//本段仅负责更新next[i]数组,不负责跳转
//如果指针+1之后,发现匹配不成功
if( pattern[i] != pattern[j] )
{
/*
情况(a):表示自匹配第一位就失配。
例如: abcabd
abcabd
b与a失配,但是当目标串中的字符与b失配时,
需要用a尝试,以免错过匹配机会。
所以next[i]= -1 + 1 = 0。
情况(b):表示找到了长度为j的等效前缀。
例如: abcabd
abcabd
d与c失配,但是找到了ab作为等效前缀,
那么当目标串中的字符与d失配时,可以用c尝试。
所以相当于"next[d]=c",遇到d失配就跳转到c。
*/
next[i] = j;
}
//如果指针+1之后,仍然匹配
else
{
/*
仍然匹配表示等效前缀在伸长,可继续循环。
等效前缀中的对应的任意两位在跳转时等效。
例如: abcabd
abcabd
即上面的ab与下面的ab的对应位在跳转时等效。
*/
next[i] = next[j];
}
}
/*
如果是等效前缀结束的位置失配,负责跳转
例如: abcabd
abcabd
d与c失配
*/
else
{
//模式串跳转到正确的位置重新开始比较
j = next[j];
}
}
}
(4.2)KMP算法
有了这个跳转表之后,KMP的代码也就不难写了。由于我们的模式串的next跳转表只需要构造一次,而且与目标串无关,因此KMP算法很适合在很多目标串中寻找同一个模式串的情况。
{
if( target.size() == 0 || pattern.size() == 0 )
return -1;
vector<int> next(pattern.size(), 0);
//自匹配,生成跳转表next
make_next(pattern, next);
size_t i = 0,j = 0;
while( i < target.size() && j < pattern.size() )
{
if( target[i] == pattern[j] )
{
++i;++j;
//如果整个模式串都匹配了,则返回
if( j == pattern.size()-1 && target[i] == pattern[j] )
return i-j;
}
//失配情况
else
{
//如果不是整个失配,则按照跳转表跳转回溯
if( next[j] != -1 )
{
j = next[j];
}
//整个失配(j=-1),则目标串也前移一位。j归零。
else
{
++i;
j = 0;
}
}
}
return -1;
}
另外可参考资料:
http://www.cppblog.com/gongzhitaao/archive/2009/03/14/76585.html 中给出了《算法导论》中KMP算法的代码。我在下篇文章中附了我注释的版本。
http://blog.csdn.net/A_B_C_ABC/archive/2005/11/25/536925.aspx 是一篇比较详尽的KMP算法讲解。