字符串匹配算法_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)
朴素匹配法的之所以效率低,说到底就是信息冗余了,没有有效利用。而后三个都是一定程度上减小了信息冗余度,提高了效率。
后三个算法,在匹配过程中,分为两步操作,第一步对字符串进行预处理,第二步再进行匹配。其中预处理是减少冗余信息的关键步骤。
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匹配
我的测试用例:
本文只是列出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算法代码
计算前缀函数PIvoid 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>
相关文章推荐
- 字符串匹配和 KMP 算法
- 字符串匹配 KMP 算法
- 字符串匹配「 KMP 算法 」
- 4种字符串匹配算法:KMP(下)
- 字符串匹配的 KMP 算法
- KMP字符串匹配算法解析
- KMP字符串匹配算法及C语言实现
- 算法-字符串匹配之KMP
- 算法模板——KMP字符串匹配
- 字符串匹配算法——BF、KMP、Sunday
- 浅谈字符串匹配的几种算法(KMP,Boyer-Moore)
- 算法——字符串匹配之KMP——看不懂算我输
- 【博客地址】:KMP字符串匹配算法与next数组
- 子字符串substring 问题 - KMP 字符串匹配算法备忘录
- 字符串匹配算法KMP实现
- 史上最浅显易懂的KMP算法讲解:字符串匹配算法
- 字符串匹配算法-kmp
- KMP(字符串匹配)算法
- KMP、BM、Sunday等字符串匹配算法及实现
- 字符串匹配算法-KMP