字符串匹配的KMP算法和Boyer-Moore算法
2016-07-24 18:49
295 查看
字符串匹配的KMP算法和Boyer-Moore算法
转自阮一峰
字符串匹配的KMP算法
字符串匹配是计算机的基本任务之一。举例来说,有一个字符串"BBC ABCDAB ABCDABCDABDE",我想知道,里面是否包含另一个字符串"ABCDABD"?
这种算法不太容易理解,网上有很多解释,但读起来都很费劲。直到读到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],后缀为,共有元素的长度为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!荣耀之路!)
<span style="font-size:18px;"> int ne fed6 xt ; char str1[M],str2 ; //str1 长,str2 短 //len1,len2,对应str1,str2的长 void get_next(int len2) { int i = 0,j = -1; next[0] = -1; while(i<len2) { if(j == -1 || str2[i] == str2[j]) { i++; j++; if(str2[i] != str2[j]) next[i] = j; else next[i] = next[j]; } else j = next[j]; } //计算某字符串的周期,如aaaa是4,abcd是1 /* int i = 0;j = -1; next[0] = -1; while(str2[i]) { if(j == -1 || str2[i] == str2[j]) { i++;j++; next[i] = j; } else j = next[j]; } len = strlen(str); i = len-j; if(len%i==0) return len/i; else return 1; */ } int kmp(int len1,int len2) { int i = 0,j = 0; get_next(len2); while(i<len1) { if(j == -1 || str1[i] == str2[j]) { i++; j++ } else j = next[j]; /* if(j == len2)//计算str2在str1中出现多少次 { cnt++; j= next[j]; } */ } //return j; //j为匹配的长度 if(j>len2) 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<m) { if(j == -1 || b[i] == b[j]) { i++; j++; if(b[i] == b[j]) next[i] = next[j]; else next[i] = j; } else j = next[j]; } } int kmp()//返回匹配位置 { int i = 0,j = 0; while(i<n) { if(a[i] == b[j]) { if(j == m-1) return i-j+1; i++; j++; } else { j = next[j]; if(j == -1) { i++; j = 0; } } } return -1; } </span>
字符串匹配的Boyer-Moore算法
上面介绍了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"不匹配。这时,[b]"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)
<span style="font-size:18px;">#include <stdio.h> #include <stdint.h> #include <stdlib.h> #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; } </span>