kmp算法和BM算法在字符串匹配中的性能比较
算法特点介绍
kmp算法:因为在bf蛮力算法中,需要嵌套两层循环。但是在匹配过程中,如果遇到失配字符是在模式字符串的中部,且失配前的一部分字符串刚好和模式字符串最前面的字符串相同,那么就可以利用失配前的匹配部分,直接将模式字符串后移,省掉模式字符串最前面部分的匹配工作。
例如:
asdfghuiyiuhljkyuiasdfghuuuuuu
当模式匹配进行到右侧的"asdfghuu"时,若发生失配,也可以确定原始字符串中有"asdfghu",这样模式字符串的前七个字符就不需要进行比较,原始字符串必定与之匹配。
通过记录模式字符串中与前缀字符串相同的关联关系,就可以在模式匹配中利用之前比较的结果,从而减少比较的次数。
建立与模式字符串等长的next数组,每次失配后访问对应的next中存储的位置。
void GetNext(char* t, int Next[])
{
int j= 1,k= 0; Next[1]= 0;
while ( j < t[0] )
if (k==0||T[j]==T[k]){
j++;
k++;
Next[j]= k;
}
else k=Next[k];
}
//时间复杂性:O(m)
也可以看出,KMP算法只适合模式字符串中容易出现重复的情况。也就是字符集比较小,模式字符串较长的情况。
BM算法:在KMP算法中,如果总是在模式字符串的后半部分失配,即使每次失败后能右移多位,但是每次匹配仍然耗时较长。
BM算法有两种策略,BC(bad character)和GS(good suffix-shift)。
- BC策略是判断在失配时失配字符下一个可能进行匹配的位置。
- 若模式字符串的前部分没有该失配字符,则可以直接将模式字符串整体右移到失配字符右侧;
- 如果模式字符串中有该字符,
如果失配字符在模式字符串中出现多次,将模式字符串右移,则取其中最靠右者。
如果模式字符串中失配字符的最右侧的位置过于靠右,使得位移值为负,则将模式字符串向右移动一位。
- GS策略是指,以后缀中多个字符为单位进行跳跃,因为如果匹配了多个字符后发生失配,所有已匹配的部分都可以最大化利用,这样即使失配,原始字符串中已匹配的部分也会被下一次匹配利用。避免了失配字符在匹配后半段造成的时间浪费。
例如:
“sdjfgjashdgfasdfgaksdgfuiweasdfasdfg”
当后半部分的"fasdfg"匹配结束后发生了失配,原始字符串的”fasdfg"部分若想要匹配成功,最可能的就是出现在模式字符串的"fasdfg"部分。
BM算法结合BC策略和GS策略从后向前进行匹配,如果出现失配字符的最右侧过于靠右,则优先使用GS表进行右移;如果出现失配字符的最右侧在左边,则选取BC表和GS表中的最大值进行右移。
测试用例生成
使用rand32()
来进行随机数的生成,分别生成长度为100、1000、10000的字符串。
随机字符串取三种取值,(0,1)串,(0-9)串和(33-125)ASCII码字符串。
代码如下
int seed;
int rand1() { return(((seed = seed * 214013L + 2531011L) >> 16) & 0x7fff); }
int rand32() {
return ((rand1() << 16) + (rand1() << 1) + rand1() % 2);
}
void getRand(int length,int rangeStart,int rangeEnd)
{
ofstream out("rand.txt", ios::app);//app表示每次操作前均定位到文件末尾
for (int i = 0; i < length; i++)
{
out<<rand32() % (rangeEnd - rangeStart) + rangeStart;
}
out<<endl;
out.close();
}
void getRand(int length,int rangeStart,int rangeEnd,bool character)
{
ofstream out("rand.txt", ios::app);//app表示每次操作前均定位到文件末尾
for (int i = 0; i < length; i++)
{
out<<char(rand32() % (rangeEnd - rangeStart) + rangeStart);
}
out<<endl;
out.close();
}
int main()
{
seed = 354;
getRand(100,0,2);
getRand(1000,0,2);
getRand(10000,0,2);
getRand(100,0,10);
getRand(1000,0,10);
getRand(10000,0,10);
getRand(100,33,126,true);
getRand(1000,33,126,true);
getRand(10000,33,126,true);
return 0;
}
生成的rand.txt文件中有9条符合条件的字符串,对于每个字符串,手动挑选前、中、后三个子字符串作为模式字符串,进行模式匹配。每次读取一个原始字符串和一个子字符串进入循环。
具体测试性能代码如下:
ifstream MyInput("rand.txt");
string a, b;
while (!MyInput.eof()){
MyInput >> a >> b;
#ifdef SHOW_INFO
//#include <windows.h> //记录时长
LARGE_INTEGER Bmt1, Bmt2,Kmpt1,Kmpt2, tc;
QueryPerformanceFrequency(&tc);
QueryPerformanceCounter(&Bmt1);
#endif // SHOW_INFO
//BM执行!!!
int bm = BM(a, b);
#ifdef SHOW_INFO
QueryPerformanceCounter(&Bmt2); //程序结束用时
BM_TIME = (double)(Bmt2.QuadPart - Bmt1.QuadPart) * 1.0 / tc.QuadPart;
#endif // SHOW_INFO
#ifdef SHOW_INFO
QueryPerformanceCounter(&Kmpt1);
#endif // SHOW_INFO
//KMP执行!!!
int kmp = indexKMP(b, a);
#ifdef SHOW_INFO
QueryPerformanceCounter(&Kmpt2); //程序结束用时
KMP_TIME = (double)(Kmpt2.QuadPart - Kmpt1.QuadPart) * 1.0 / tc.QuadPart;
#endif // SHOW_INFO
//输出结果
printf("BM算法的结果为:%d\nKMP算法的结果为:%d\n", bm,kmp);
#ifdef SHOW_INFO
cout << "BM比较次数为:" << BM_COMPARE << endl<< "BM花费时间为:" << BM_TIME << endl;
cout << "KMP比较次数为:" << KMP_COMPARE <<endl<< "KMP花费时间为:" << KMP_TIME << endl;
KMP_COMPARE = 0;
BM_COMPARE = 0;
#endif // SHOW_INFO
}
MyInput.close();
全部代码见BM.cpp和随机数生成.cpp
测试
字符串种类 | 字符串长度 | 匹配结果 | KMP耗时(单位0.1ms) | BM耗时(单位0.1ms) | KMP比较次数 | BM比较次数 |
---|---|---|---|---|---|---|
0,1串 | 100 | 1 | 1.3996e-5 | 2.14605e-5 | 11 | 11 |
0,1串 | 100 | 48 | 4.99198e-5 | 6.11157e-5 | 61 | 63 |
0,1串 | 100 | 85 | 7.74443e-5 | 7.04463e-5 | 97 | 67 |
0,1串 | 1000 | 26 | 0.000220203 | 0.000170284 | 130 | 141 |
0,1串 | 1000 | 496 | 0.000318175 | 0.000612556 | 595 | 879 |
0,1串 | 1000 | 816 | 0.000647546 | 0.000918601 | 900 | 1225 |
0,1串 | 10000 | 1352 | 0.00138607 | 0.0025258 | 2255 | 1626 |
0,1串 | 10000 | 5061 | 0.00313929 | 0.00586337 | 5980 | 8416 |
0,1串 | 10000 | 8582 | 0.00528487 | 0.00489858 | 9372 | 7777 |
0-9串 | 100 | 6 | 2.00609e-5 | 3.63895e-5 | 20 | 5 |
0-9串 | 100 | 37 | 3.91887e-5 | 3.40568e-5 | 50 | 20 |
0-9串 | 1000 | 131 | 0.000549574 | 0.000321907 | 210 | 88 |
0-9串 | 1000 | 395 | 0.00025426 | 0.000152556 | 474 | 125 |
0-9串 | 1000 | 711 | 0.000411014 | 0.000197809 | 847 | 212 |
0-9串 | 10000 | 1872 | 0.0011528 | 0.000754382 | 2425 | 777 |
0-9串 | 10000 | 4852 | 0.00316029 | 0.00169771 | 5588 | 1481 |
0-9串 | 10000 | 9505 | 0.00465785 | 0.00148684 | 9821 | 1034 |
33-126的符号串 | 100 | 4 | 8.8641e-6 | 1.21298e-5 | 14 | 11 |
33-126的符号串 | 100 | 39 | 7.83773e-5 | 7.46451e-5 | 48 | 4 |
33-126的符号串 | 100 | 80 | 3.63895e-5 | 2.00609e-5 | 88 | 18 |
33-126的符号串 | 1000 | 76 | 8.30426e-5 | 8.67749e-5 | 147 | 73 |
33-126的符号串 | 1000 | 447 | 0.000392353 | 0.00012503 | 509 | 75 |
33-126的符号串 | 1000 | 876 | 0.000644747 | 0.000132495 | 931 | 79 |
33-126的符号串 | 10000 | 297 | 0.000196876 | 0.000123164 | 399 | 108 |
33-126的符号串 | 10000 | 4034 | 0.00402524 | 0.00107442 | 5020 | 1043 |
33-126的符号串 | 10000 | 8927 | 0.00669333 | 0.000711927 | 9070 | 313 |
结论
- 当模式字符串中字符种类较少时,KMP算法略微优于BM算法,两种算法比较次数都较多。
- 当原始字符串较长时,BM算法明显优于KMP算法,KMP算法比较次数随原始字符串长度增长较快。
- 当模式字符串中字符种类比较多时,BM算法明显优于KMP算法,比较次数增长较慢。