单模式字符串匹配
1. 朴素算法
朴素算法的问题在于不够智能,有些位置明显没有必要进行比较操作,但这个算法无法区分出来,还是继续比较,浪费了资源。
2. KMP算法
在KMP算法中,引入了前缀函数的概念,从而可以更加精确的知道:当不匹配发生时,应该跳过多少个字符。下面介绍前缀函数。
字符串A = "abcde" B = "ab"。 那么就称字符串B为A的前缀,记为B ⊏ A。同理可知 C = "e","de" 等都是 A 的后缀,以为C ⊐ A。
这里模式串 P = “ababaca”,在匹配了 q=5 个字符后失配,因此,下一步就是要考虑将P向右移多少位进行新的一轮匹配检测。朴素算法中,直接将P右移1位,也就是将P的首字符'a'去和目标串的'b'字符进行检测,这明显是多余的。通过我们肉眼的观察,可以很简单的知道应该将模式串P右移到下图'a3'处再开始新一轮的检测,直接跳过肯定不匹配的字符'b',那么我们“肉眼”观察的这一结果怎么把它用语言表示出来呢?
我们的观察过程是这样的:
1. P的前缀"ab"中'a' != 'b',又因该前缀已经匹配了T中对应的"ab",因此,该前缀的字符'a1'肯定不会和T中对应的字串"ab"中的'b'匹配,也就是将P向右滑动一个位移是无意义的。
2. 接下来考察P的前缀"aba",发现该前缀自身的前缀'a1'与自身后缀'a2'相等,"a1 b a2" 已经匹配了T中的"a b a3",因此有 'a2' == 'a3', 故得到 'a1' == 'a3'......
3. 利用此思想,可推知在已经匹配 q=5 个字符的情况下,将P向右移 当且仅当 2个位移时,才能满足既没有冗余(如把'a'去和'b'比较),又不会丢失(如把'a1' 直接与 'a4' 开始比较,则丢失了与'a3'的比较)。
4. 而前缀函数就是这样一种函数,它决定了q与位移的一一对应关系,通过它就可以间接地求得位移s。
这样的观察过程并不具有一般性,下面是《算法导论》中对前缀函数的形式化说明:
已知一个模式P[1. . m],模式P的前缀函数是函数π{1,2,. . . , m}->{0,1, 2,. . . ,m-1}并满足
π[q]=max{k:k<q 且Pk⊐ Pq}
即π[q]是Pq的真后缀P的最长前缀的长度(此是《算法导论》中原话,但不是很好理解,其实就是Pq中即是自己的真后缀,又是自己最长前缀的字符串的最大长度)。下面举例说明(模式P=ababababca)
i=1时,a真后缀为空;i=2时,ab真后缀为b,不是自己的前缀;i=3时,aba真后缀为a, ab,且a和ab都是aba的前缀,ab最长,故为2;。。。
KMP算法中,如果q+1时发生不匹配,则可以向前移动q-π[q]位。
using namespace std;
/*
when searching a pattern in a string, and mismatch happened, we can skip more chars, instead of going through one by one;
the skip rule is that:
1. if position p mismatched, we need consider the chars in 0- (p-1);
2. whether [0,k-1](prefix substring) matched with [p-k,p-1](suffix substring),if matched, we can align the pattern to p-k;and do comparation from p again.
below function is get the k for different p, more information refer to comments inline;
*/
void get_skippattern(char *pattern, int* next, int len)
{
int pos = 2;
int subStrIndex = 0; //valid prefix candidate substring index;
next[0] = -1; // when 1st char mismatched, always move 1 (p=0, k=-1);
next[1] = 0; // when 2nd mismatched, always move 1(p=1, k=0);in fact, if the 2nd char is same as 1st char, we can move 2
while(pos<len)
{
if(pattern[pos - 1] == pattern[subStrIndex]) //one char matched, then continue to match more,
{
subStrIndex++; //prefix substring move ahead;
next[pos] = subStrIndex;//for current position, the k is got;
pos++; //current pos move ahead;
}
else if(subStrIndex>0) //one substring found, but in the new pos, mismatched;
{
subStrIndex = next[subStrIndex]; //then we need fall back subStrIndex to value that still can be matched;
}
else
{
next[pos] = 0;
pos++;
}
}
for(int i =0;i<len;i++)
cout<<next[i]<<" ";
}
int KMP_search(char *src, int slen, char *pattern, int plen)
{
int* next = (int *)malloc(sizeof(int)*slen);
get_skippattern(pattern,next,plen);
int indexInSrc = 0;
int offset = 0;
while((indexInSrc+offset)<slen)
{
if(pattern[offset] == src[indexInSrc+offset])
{
if(offset == (plen-1))
return indexInSrc;
offset++;
}else
{
indexInSrc += offset-next[offset];
if(next[offset]>-1)
offset = next[offset];
else
offset = 0;
}
}
return slen;
}
int main (int argc, char ** argv)
{
//char *pat = "ABCDABDEF";
//char *src = "ABC ABCDAB ABCDABCDABDEF";
char *pat="ABABETTABABABYUABCD";
char *src = "ABCDEABCDGABCDETTABCDFABCDETTABCDATYUABCD";
int index = KMP_search(src,strlen(src),pat,strlen(pat));
cout<<"found pattern in "<<index<<endl;
return 0;
}
3. BM算法
BM算法的特殊之处在于BM是右向左匹配,同时结合坏字符和好后缀两个规则使得移动距离最大。下面分别介绍坏字符和好后缀规则:
好后缀算法
如果程序匹配了一个好后缀, 并且在模式中还有另外一个相同的后缀, 那
把下一个后缀移动到当前后缀位置。好后缀算法有两种情况:
Case1:模式串中有子串和好后缀安全匹配,则将最靠右的那个子串移动到好后缀的位置。继续进行匹配。
Case2:如果不存在和好后缀完全匹配的子串,则在好后缀中找到具有如下特征的最长子串,使得P[m-s…m]=P[0…s]。说不清楚的看图。
给一些具体的例子
坏字符算法
当出现一个坏字符时, BM算法向右移动模式串, 让模式串中最靠右的对应字符与坏字符相对,然后继续匹配。坏字符算法也有两种情况。
Case2:模式串中不存在坏字符。见图。
移动规则
BM算法的移动规则是:
将概述中的++j,换成j+=MAX(shift(好后缀),shift(坏字符)),即BM算法是每次向右移动模式串的距离是,按照好后缀算法和坏字符算法计算得到的最大值。
shift(好后缀)和shift(坏字符)通过模式串的预处理数组的简单计算得到。好后缀算法的预处理数组是bmGs[],坏字符算法的预处理数组是BmBc[]。
下面先解释这两个数组的意义:
BmBc 的定义:
1、 字符在模式串中没有出现:,如模式串中没有字符p,则BmBc[‘p’] = strlen(模式串)。
2、 字符在模式串中有出现。如下图,BmBc[‘k’]表示字符k在模式串中最后一次出现的位置,距离模式串串尾的长度。
如果只考虑坏字符,应该移动多少呢?下面的图里有3个例子:
示例1中,在b和c比较时发生了不match,这时,我们的BmBc[‘c’] = 3,这时我们应该移动多少呢,移动-1,如何计算的呢? BmBc[‘c’] – strlen(pat) +1 + i (index of pattern string)
实例2中,b和a发生不match,这时,BmBc[‘a’] = 6, 应该移动6-7+1+2 = 2;
实例3中,b和y发生不match,这时,BmBc[‘y’] = 7,应该移动7-7+1+1 = 2;
对于这里BmBc的定义,和shift的值的计算是很难让人理解的,我们是不是可以简单一点定义BmBc[char] 表示char在pattern中最后出现的位置,如果不出现为pattern的长度,i是不match的index,shift的距离就是BmBc[char]-i。
还有就是,在实例1中,我们真的要去移动-1吗,其实没有必要了,如果只用坏字符,你可以想想怎么做;但如果考虑上好后缀就不用额外考虑了。
为了实现好后缀规则,需要定义一个数组suffix[],其中suffix[i] = s 表示以i为起点(包含i,从右往左匹配),与模式串后缀匹配的最大长度,如下图所示,用公式可以描述:满足P[i-s, i] == P[m-s, m]的最大长度s。
计算suffix的代码如下所示:
for (i=m-2;i>=0;--i)
{
q=i;
while(q>=0&&P[q]==P[m-1-i+q])
--q;
suffix[i]=i-q;
}
有了suffix[i],如何计算BmGc?
bmGs的定义(BmGs数组的下标是数字,表示字符在模式串中位置), BmGs数组的定义,分三种情况:
在这个视角图1中,在i处发生不匹配,从i开始从右向左搜索子字符串,在视图2中试图找到不匹配的字符和子串匹配位置的关系。视图2中i开始的子串与后缀匹配,那可以知道后缀的长度就是Suffix[i],再往左移动一下就是不匹配的位置了,而这时应该移动的距离是m-1-i,也就是式子bmGs[m-1-suff[i]] = m- 1 – i;
在这种情况下,空白位置发生不匹配时,其好后缀都是最前面的两个,那么其移动的距离其实跟不匹配的位置 j 没有关系,只与最好前缀的位置i有关,所以,bmGs[j] = m- 1 – i;
没有任何子串匹配的时候,那就移动模式串的长度。
举例如下:
实现代码如下:
int i, j, suff[XSIZE];
suffixes(x, m, suff);
for (i = 0; i < m; ++i)
j = 0;
if (suff[i] == i + 1)
for (; j < m - 1 - i; ++j)
if (bmGs[j] == m)
bmGs[j] = m - 1 - i;
for (i = 0; i <= m - 2; ++i)
bmGs[m - 1 - suff[i]] = m - 1 - i;
}
下面来完整实现一下BM算法吧:
#include <stdio.h>
#include <string.h>
const int CHAR_COUNT = 26; // only lower case ASCII char
void calculateBmBc(const char *s, int len, int *BmBc)
{
int i=0;
for(i = 0; i<CHAR_COUNT;i++)
{
BmBc[i] = len;
}
for(i = 0;i<len;i++)
{
BmBc[s[i]-'a'] = i;
}
}
void calculateSuffix(const char *s, int len,int *suffix)
{
int i = len -1;
int j = 0;
suffix[len-1] = len;
for(;i>=0;i--)
{
j = 0;
while(j<(len-1) && s[i-j] == s[len-1-j])
j++;
suffix[i] = j;
}
}
void calculateBmGs(const char *s, int len, int *BmGs)
{
int* suffix = (int *)malloc(sizeof(int)*len);//new int[len];
int i = 0;
int j = 0;
calculateSuffix(s,len,suffix);
for(i=0;i<len;i++)// init the array, and also cover case 3
{
BmGs[i] = len;
}
for(i=len-1;i>=0;i--)
{
if(suffix[i] == i+1) // prefix of the string matched with suffix, case 2
{
for(j = 0;j<len-1-i;j++)
{
if(BmGs[j] == len)
BmGs[j] = len - 1 - i;
}
}
}
for(i = 0;i<=len-2;i++) // case 1;
{
BmGs[len-1-suffix[i]] = len-1-i;
}
free(suffix);
}
int BMSearch(const char *src,int srclen, const char *pattern, int patlen)
{
int i= 0;
int * BmGs = (int *)malloc(sizeof(int)*(patlen));
int * BmBc = (int *)malloc(sizeof(int)*(CHAR_COUNT));
calculateBmBc(pattern, patlen, BmBc);
calculateBmGs(pattern,patlen, BmGs);
for(i = 0;i<(srclen-patlen);)
{
int j = patlen-1;
while(j>=0 && src[i+j] == pattern[j]) j--;
if(j <0)
return i;
else
i += BmGs[j]>(j-BmBc[j])?BmGs[j]:(j-BmBc[j]);
}
free(BmGs);
free(BmBc);
return srclen;
}
int main(int argc, char **argv)
{
int i = BMSearch("GCAGAGAG",8,"AGAG",4);
printf("found pattern at %d",i);
return 0;
}
4. Sunday算法
多模式字符串匹配
1. AC
2. Wu-Manber算法
Reference:
1. http://blog.csdn.net/zdl1016/article/details/4654061
2. http://blog.csdn.net/iJuliet/article/details/4200771
4. http://hi.baidu.com/kmj0217/blog/item/6f837f2f3da097311e3089cb.html
5. http://www.cs.utexas.edu/users/moore/best-ideas/string-searching/index.html
6. http://blog.sina.com.cn/s/blog_6cf48afb0100n561.html
7. http://blog.csdn.net/sealyao/article/details/4568167
8. http://www.cnblogs.com/v-July-v/archive/2011/06/15/2084260.html