转自 阮一峰
字符串匹配是计算机的基本任务之一。
举例来说,有一个字符串"BBC ABCDAB ABCDABCDABDE",我想知道,里面是否包含另一个字符串"ABCDABD"?
许多算法可以完成这个任务,Knuth-Morris-Pratt算法(简称KMP)是最常用的之一。它以三个发明者命名,起头的那个K就是著名科学家Donald Knuth。
这种算法不太容易理解,网上有很多解释,但读起来都很费劲。直到读到Jake Boxer的文章,我才真正理解这种算法。下面,我用自己的语言,试图写一篇比较好懂的KMP算法解释。
1.
首先,字符串"BBC ABCDAB ABCDABCDABDE"的第一个字符与搜索词"ABCDABD"的第一个字符,进行比较。因为B与A不匹配,所以搜索词后移一位。
2.
因为B与A不匹配,搜索词再往后移。
3.
就这样,直到字符串有一个字符,与搜索词的第一个字符相同为止。
4.
接着比较字符串和搜索词的下一个字符,还是相同。
5.
直到字符串有一个字符,与搜索词对应的字符不相同为止。
6.
这时,最自然的反应是,将搜索词整个后移一位,再从头逐个比较。这样做虽然可行,但是效率很差,因为你要把"搜索位置"移到已经比较过的位置,重比一遍。
7.
一个基本事实是,当空格与D不匹配时,你其实知道前面六个字符是"ABCDAB"。KMP算法的想法是,设法利用这个已知信息,不要把"搜索位置"移回已经比较过的位置,继续把它向后移,这样就提高了效率。
8.
怎么做到这一点呢?可以针对搜索词,算出一张《部分匹配表》(Partial Match Table)。这张表是如何产生的,后面再介绍,这里只要会用就可以了。
9.
已知空格与D不匹配时,前面六个字符"ABCDAB"是匹配的。查表可知,最后一个匹配字符B对应的"部分匹配值"为2,因此按照下面的公式算出向后移动的位数:
移动位数 = 已匹配的字符数 - 对应的部分匹配值
因为 6 - 2 等于4,所以将搜索词向后移动4位。
10.
因为空格与C不匹配,搜索词还要继续往后移。这时,已匹配的字符数为2("AB"),对应的"部分匹配值"为0。所以,移动位数 = 2 - 0,结果为 2,于是将搜索词向后移2位。
11.
因为空格与A不匹配,继续后移一位。
12.
逐位比较,直到发现C与D不匹配。于是,移动位数 = 6 - 2,继续将搜索词向后移动4位。
13.
逐位比较,直到搜索词的最后一位,发现完全匹配,于是搜索完成。如果还要继续搜索(即找出全部匹配),移动位数 = 7 - 0,再将搜索词向后移动7位,这里就不再重复了。
14.
下面介绍《部分匹配表》是如何产生的。
首先,要了解两个概念:"前缀"和"后缀"。 "前缀"指除了最后一个字符以外,一个字符串的全部头部组合;"后缀"指除了第一个字符以外,一个字符串的全部尾部组合。
15.
"部分匹配值"就是"前缀"和"后缀"的最长的共有元素的长度。以"ABCDABD"为例,
- "A"的前缀和后缀都为空集,共有元素的长度为0;
- "AB"的前缀为[A],后缀为[B],共有元素的长度为0;
- "ABC"的前缀为[A, AB],后缀为[BC, C],共有元素的长度0;
- "ABCD"的前缀为[A, AB, ABC],后缀为[BCD, CD, D],共有元素的长度为0;
- "ABCDA"的前缀为[A, AB, ABC, ABCD],后缀为[BCDA, CDA, DA, A],共有元素为"A",长度为1;
- "ABCDAB"的前缀为[A, AB, ABC, ABCD, ABCDA],后缀为[BCDAB, CDAB, DAB, AB, B],共有元素为"AB",长度为2;
- "ABCDABD"的前缀为[A, AB, ABC, ABCD, ABCDA, ABCDAB],后缀为[BCDABD, CDABD, DABD, ABD, BD, D],共有元素的长度为0。
16.
"部分匹配"的实质是,有时候,字符串头部和尾部会有重复。比如,"ABCDAB"之中有两个"AB",那么它的"部分匹配值"就是2("AB"的长度)。搜索词移动的时候,第一个"AB"向后移动4位(字符串长度-部分匹配值),就可以来到第二个"AB"的位置。
附上 KMP算法模板:(链接:ACM!荣耀之路!) int next[N];
char str1[M],str2[N];
//str1 长,str2 短
//len1,len2,对应str1,str2的长
void get_next(int len2)
{
int i = 0,j = -1;
next[0] = -1;
while(ilen2)
return 1;//这里也可以返回i-len2来获得匹配在主串中开始的位置
else
return 0;
}
//数字KMP
int a[1000005],b[10005];
int next[10005],n,m;
void getnext()
{
int i = 0,j = -1;
next[0] = -1;
while(i
上面介绍了KMP算法。
但是,它并不是效率最高的算法,实际采用并不多。各种文本编辑器的"查找"功能(Ctrl+F),大多采用Boyer-Moore算法。
Boyer-Moore算法不仅效率高,而且构思巧妙,容易理解。1977年,德克萨斯大学的Robert S. Boyer教授和J Strother Moore教授发明了这种算法。
下面,我根据Moore教授自己的例子来解释这种算法。
1.
假定字符串为"HERE IS A SIMPLE EXAMPLE",搜索词为"EXAMPLE"。
2.
首先,"字符串"与"搜索词"头部对齐,从尾部开始比较。
这是一个很聪明的想法,因为如果尾部字符不匹配,那么只要一次比较,就可以知道前7个字符(整体上)肯定不是要找的结果。
我们看到,"S"与"E"不匹配。这时,"S"就被称为"坏字符"(bad character),即不匹配的字符。我们还发现,"S"不包含在搜索词"EXAMPLE"之中,这意味着可以把搜索词直接移到"S"的后一位。
3.
依然从尾部开始比较,发现"P"与"E"不匹配,所以"P"是"坏字符"。但是,"P"包含在搜索词"EXAMPLE"之中。所以,将搜索词后移两位,两个"P"对齐。
4.
我们由此总结出"坏字符规则":
后移位数 = 坏字符的位置 - 搜索词中的上一次出现位置
如果"坏字符"不包含在搜索词之中,则上一次出现位置为 -1。
以"P"为例,它作为"坏字符",出现在搜索词的第6位(从0开始编号),在搜索词中的上一次出现位置为4,所以后移 6 - 4 = 2位。再以前面第二步的"S"为例,它出现在第6位,上一次出现位置是 -1(即未出现),则整个搜索词后移
6 - (-1) = 7位。
5.
依然从尾部开始比较,"E"与"E"匹配。
6.
比较前面一位,"LE"与"LE"匹配。
7.
比较前面一位,"PLE"与"PLE"匹配。
8.
比较前面一位,"MPLE"与"MPLE"匹配。我们把这种情况称为"好后缀"(good suffix),即所有尾部匹配的字符串。注意,"MPLE"、"PLE"、"LE"、"E"都是好后缀。
9.
比较前一位,发现"I"与"A"不匹配。所以,"I"是"坏字符"。
10.
根据"坏字符规则",此时搜索词应该后移 2 - (-1)= 3 位。问题是,此时有没有更好的移法?
11.
我们知道,此时存在"好后缀"。所以,可以采用"好后缀规则":
后移位数 = 好后缀的位置 - 搜索词中的上一次出现位置
举例来说,如果字符串"ABCDAB"的后一个"AB"是"好后缀"。那么它的位置是5(从0开始计算,取最后的"B"的值),在"搜索词中的上一次出现位置"是1(第一个"B"的位置),所以后移 5 - 1 = 4位,前一个"AB"移到后一个"AB"的位置。
再举一个例子,如果字符串"ABCDEF"的"EF"是好后缀,则"EF"的位置是5 ,上一次出现的位置是 -1(即未出现),所以后移 5 - (-1) = 6位,即整个字符串移到"F"的后一位。
这个规则有三个注意点:
(1)"好后缀"的位置以最后一个字符为准。假定"ABCDEF"的"EF"是好后缀,则它的位置以"F"为准,即5(从0开始计算)。
(2)如果"好后缀"在搜索词中只出现一次,则它的上一次出现位置为 -1。比如,"EF"在"ABCDEF"之中只出现一次,则它的上一次出现位置为-1(即未出现)。
(3)如果"好后缀"有多个,则除了最长的那个"好后缀",其他"好后缀"的上一次出现位置必须在头部。比如,假定"BABCDAB"的"好后缀"是"DAB"、"AB"、"B",请问这时"好后缀"的上一次出现位置是什么?回答是,此时采用的好后缀是"B",它的上一次出现位置是头部,即第0位。这个规则也可以这样表达:如果最长的那个"好后缀"只出现一次,则可以把搜索词改写成如下形式进行位置计算"(DA)BABCDAB",即虚拟加入最前面的"DA"。
回到上文的这个例子。此时,所有的"好后缀"(MPLE、PLE、LE、E)之中,只有"E"在"EXAMPLE"还出现在头部,所以后移 6 - 0 = 6位。
12.
可以看到,"坏字符规则"只能移3位,"好后缀规则"可以移6位。所以,Boyer-Moore算法的基本思想是,每次后移这两个规则之中的较大值。
更巧妙的是,这两个规则的移动位数,只与搜索词有关,与原字符串无关。因此,可以预先计算生成《坏字符规则表》和《好后缀规则表》。使用时,只要查表比较一下就可以了。
13.
继续从尾部开始比较,"P"与"E"不匹配,因此"P"是"坏字符"。根据"坏字符规则",后移 6 - 4 = 2位。
14.
从尾部开始逐位比较,发现全部匹配,于是搜索结束。如果还要继续查找(即找出全部匹配),则根据"好后缀规则",后移 6 - 0 = 6位,即头部的"E"移到尾部的"E"的位置。
附上Boyer-Moore模板(链接:Seiyagoo)
#include
#include
#include
#define ALPHABET_LEN 256
uint32_t patlen;
#define NOT_FOUND patlen
#define max(a, b) ((a < b) ? b : a)
/*构造Bc表*/
void make_delta1(int *delta1, uint8_t *pat, int32_t patlen) {
int i;
/*初始化整个字符表的shift值为模式串P的长度(即case 2:出现坏字符时,P中无相同的字符)*/
for (i=0; i < ALPHABET_LEN; i++) {
delta1[i] = NOT_FOUND;
}
/*从左至右更新相同字符离失配位置(即patlen-1)的最近距离(case 1)*/
for (i=0; i < patlen-1; i++) {
delta1[pat[i]] = patlen-1 - i;
}
}
/*Gs规则case 2:suffix-prefix对,从已匹配后缀[pos, wordlen)判断word是否存在前缀,
即word[0, suffixlen) == word[pos, wordlen)
*/
int is_prefix(uint8_t *word, int wordlen, int pos) {
int i;
int suffixlen = wordlen - pos;
// could also use the strncmp() library function here
for (i = 0; i < suffixlen; i++) {
if (word[i] != word[pos+i]) {
return 0;
}
}
return 1;
}
/*Gs规则case 1:suffix-suffix对,从pos向←查找与从P末尾(即已匹配后缀)向←查找相等的最长后缀,
并返回最长后缀的长度
*/
int suffix_length(uint8_t *word, int wordlen, int pos) {
int i;
// increment suffix length i to the first mismatch or beginning of the word
//比较范围[1, pos]与[patlen-pos, patlen-1], 注意:串word[0..pos]的后缀不包含自身
for (i = 0; (word[pos-i] == word[wordlen-1-i]) && (i < pos); i++);
return i;
}
/*构造Gs表*/
void make_delta2(int *delta2, uint8_t *pat, int32_t patlen) {
int p;
int last_prefix_index = patlen-1;
/*first loop:Gs规则case 2*/
for (p=patlen-1; p>=0; p--) {
if (is_prefix(pat, patlen, p+1)) { //从p+1开始的后缀是否存在前缀(p失配)
last_prefix_index = p+1; //last_prefix_index记录从右至左最后一个匹配字符的index(即p的右边)
}
//若存在前缀,保存最后一个匹配字符的index;否则,保存上次已匹配字符的index
delta2[p] = last_prefix_index; //@bug 1: + (patlen-1 - p);
}
/*
second loop:Gs规则case 1,因为case 2是前缀,而中间的子串(可以看做[0,p]的suffix)也可能=P的suffix,
且有可能不止一个中间子串,故p从左向后进行处理,保存最靠近P的suffix的对应子串前一个字符的shift长度
*/
for (p=0; p < patlen-1; p++) {
int slen = suffix_length(pat, patlen, p); //末尾向左对应的从p向左的最长后缀的长度
/*
若已匹配suffix-suffix对的前导字符不匹配,保存向左的第一个失配字符的shift长度
(即suffix-suffix对的起始位置之差)。若匹配,则为前缀即case 2,无需改变shift值
*/
if (slen > 0 && pat[p - slen] != pat[patlen-1 - slen]) {
//slen=0, 即case 3:delta2[patlen-1-slen]=delta2[patlen-1]=patlen
delta2[patlen-1 - slen] = patlen-1 - p ; //@bug 2: + slen;
}
}
}
/*打印预处理得到的Bc表和Gs表*/
void print_pre_table(int *delta1, int *delta2, uint8_t *pat, uint32_t patlen){
uint32_t i;
printf("模式串:%s\n", pat);
printf("坏字符shift表:\n");
for (i=0; i < patlen-1; i++) {
printf("(%c, %d)\n", pat[i], delta1[pat[i]]);
}
printf("(其他字符, %d)\n", NOT_FOUND);
printf("\n好后缀shift表:\n");
for (i=0; i < patlen; i++) {
printf("(%u, %d)\n", i, delta2[i]);
}
}
/*BM算法主框架*/
uint8_t boyer_moore (uint8_t *string, uint32_t stringlen, uint8_t *pat, uint32_t patlen) {
uint32_t i;
int delta1[ALPHABET_LEN];
int *delta2 = (int *)malloc(patlen * sizeof(int));
make_delta1(delta1, pat, patlen);
make_delta2(delta2, pat, patlen);
print_pre_table(delta1, delta2, pat, patlen);
i = patlen-1;
while (i < stringlen) {
int j = patlen-1;
while (j >= 0 && (string[i] == pat[j])) {
--i;
--j;
}
if (j < 0) {
free(delta2);
return i+1; //返回T中匹配的位置
}
i += max(delta1[string[i]], delta2[j]);
//j失配( [j+1, patlen)已匹配 ),
// i向右移动的距离取主串T中坏字符delta1[string[i]]与模式串P中好后缀delta2[j]的大者
}
free(delta2);
return -1;
}
int main()
{
uint8_t pat[]="abracadabra";
uint8_t txt[]="abracadabtabradabracadabcadaxbrabbracadabraxxxxxxabracadabracadabra";
patlen = sizeof(pat)/sizeof(pat[0]) - 1;
uint32_t n = sizeof(txt)/sizeof(txt[0]) - 1;
uint8_t ans=boyer_moore(txt, n, pat, patlen);
printf("\n匹配位置:%d\n", ans);
return 0;
}