您的位置:首页 > 其它

字符串匹配算法_KMP

2013-04-20 10:37 330 查看
昨天初步学习了下字符串匹配的算法,有朴素匹配法(又称Brute-Force)、Rabin-Karp算法、有限自动机匹配法 以及 KMP算法。
本文只是列出KMP算法的代码,以及我个人的一点儿肤浅的理解。我是做好准备,将来理解得更深入时,会更新本文。

本文代码又是基本照搬别人的>_<。。原文在此 http://billhoo.blog.51cto.com/2337751/411486

约定:模式串Pattern,记为P,其长度为m;目标串Test,记为T,其长度为n;字符的集合记为Σ

隐含假设:模式串一般比目标串短,故m = O(n)

四种算法对比分析:

朴素匹配法BF
朴素匹配法的之所以效率低,说到底就是信息冗余了,没有有效利用。而后三个都是一定程度上减小了信息冗余度,提高了效率。

后三个算法,在匹配过程中,分为两步操作,第一步对字符串进行预处理,第二步再进行匹配。其中预处理是减少冗余信息的关键步骤。

Rabin-Karp
Rabin-Karp在匹配时利用子串的hash值的比较,达到匹配的目的。这本身并没有带来冗余度的减少,但是因为计算某一个子串A的hash值之后,能够根据它的值,在很短时间内计算出下一个子串B的hash值,从而提高匹配效率。这里的信息冗余就是子串A和子串B重叠那一部分子串的hash值,减少了不必要的第二遍计算。当然,我们要考虑到模式串Pattern可能比较长,如果把它看做|Σ|进制的一个数,可能会太大不方便存储。于是,改进的版本,就利用数论知识,通过每一步计算都对某个常数q取余,降低数的数量级。这样一来,不会影响到本来就匹配的所有命中点,但是会增加一些伪命中点。当遇到一个可能的命中点时,通过最朴素的方法——逐位的字符匹配(运行时间O(m)),来进一步验证到底是命中点还是伪命中点。

去伪办法还是逐位匹配,而最坏的情况是每次都伪命中,每次都要逐位匹配,所以Rabin-Karp算法的最坏运行时间还是O(m*n)。期望运行时间比较难算,近似认为伪命中的概率等于从1到q的q个数中选中q的概率,即为1/q,如此 则期望运行时间为O(n) + O(m(v + n/q))。其中q是选取的一个常数(一般为素数),v是有效位移数。如果选取q >= m且v = O(1) ,则期望运行时间为O(n)。

有限自动机匹配法FAM 
FAM方法,借助有限自动机,为字符串的所有可能匹配过程建立状态和转移。其中包含5个要素:Q, q0, A, Σ 和 δ 。

Q为状态的有限集合,q0为初始状态, A是接受状态的集合, Σ 是字符的集合(也称输入字母表), δ 是一个从Q * Σ到Q的函数,成为有限自动机的转移函数。

δ (i, a) = j 即表示状态i接受字符a就转入状态j。

预处理阶段,FAM方法可以在O(m^3 * |Σ|)时间内(改进版本可以做到O(m * |Σ|)的时间),计算出全部的状态转移函数。然后在匹配阶段,利用状态转移函数,在O(n)时间内进行匹配。改进版本的总时间为 O(m * |Σ|) + O(n)。

Knuth-Morris-Pratt算法KMP
KMP算法跟FAM方法有类似的思想,只不过它在预处理阶段可以用O(m)的时间计算出前缀函数PI,处理时间是O(n),故总时间O(m) + O(n) = O(n)。

KMP比FAM高效一点儿,后者因为要对每一个字符a∈Σ都进行计算,故预处理时间多了一个Σ因子。

KMP算法代码

计算前缀函数PI

void computePrefixFunc(char *P, char *PI, int sizeP, int sizePI)
{
int q = 2;//已处理的字符数

/*PI数组大小为sizePI == sizeP + 1,
其中PI[0]无意义不使用, PI[1] == 0是很自然的事*/
PI[1] = 0;

int k = 0;//当前前缀长度

for(;q <= sizeP;q++)
{
/*如果当前不匹配,递归地赋值为
其前缀位置*/
while(k > 0 && P[k] != P[q - 1])
{
k = PI[k];
}

/*如果匹配,前缀长度加一*/
if(P[k] == P[q-1])
{
k++;
}

/*记录q位置的当前前缀值*/
PI[q] = k;

}

}


KMP匹配

void KMP_match(char *P, char *PI, char *T, int sizeP, int sizePI, int sizeT)
{
computePrefixFunc(P, PI, sizeP, sizePI);

int i = 0;
int q = 0;//当前匹配成功的长度

for(;i < sizeT;i++)
{
/*如果当前匹配不成功,递归地查看其前缀位置,
直到匹配成功或者q == 0*/
while(q > 0 && P[q] != T[i])
{
q = PI[q];
}

/*如果匹配,则匹配长度加一*/
if(T[i] == P[q])
{
q++;
}

/*如果匹配长度达到模式串的长度,则找到了一个成功的匹配,
此时将匹配长度置为其前缀长度,做好准备接受下一个匹配*/
if(q == sizeP)
{
std::cout<<"String matched with shift "<<i - sizeP + 1<<std::endl;
q = PI[q];
}
}

}

我的测试用例:

#include <iostream>
#include <cstdlib>
#include <ctime>

#define P_SIZE 5
#define T_SIZE 300000
#define MAX_FOR_RAND 10

int main(int argc, char* argv[])
{
char pArr[P_SIZE];
char tArr[T_SIZE];
char PI[P_SIZE];
srand((unsigned int)time(NULL));//生成伪随机数种子

/*随机生成模式串和目标串*/
for(int i = 0;i < P_SIZE;i++)
{
pArr[i] = 'a' + rand() % MAX_FOR_RAND;
}

for(int i = 0;i < T_SIZE;i++)
{
tArr[i] = 'a' + rand() % MAX_FOR_RAND;
}

/*计算KMP_match运行的时间*/
clock_t start_time = clock();

KMP_match(pArr, PI, tArr, P_SIZE, P_SIZE + 1, T_SIZE);

clock_t end_time = clock();

double cost = (double)(end_time - start_time) / CLOCKS_PER_SEC * 1000;

std::cout<<"Running time is "<<cost<<" ms."<<std::endl;

return 0;
}</ctime></cstdlib></iostream>
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: