摘要
分别用KMP、MonteCarlo、LasVegs算法编制三个程序,随机生成不小于5000对长度不等的01串(三个程序使用相同的串),然后统计算法的执行时间和MonteCarlo算法的出错比率,并根据运行结果对三种算法进行深入的比较。
KMP算法
KMP算法的主要特点是指向主串的指针不需要回溯,只向右移动,即模式串在与主串失配时,并不回溯主串的指针与模式串重新匹配,而是根据已经得到的匹配信息将模式串尽可能远的向右滑动一段。滑动距离的大小取决于模式串的失效函数next, next[k](0<=k<=m-1)的值表示当模式串在下标为k的字符处与主串失配时应该向右移动到下标为next[k]的字符处再与主串重新匹配。算法首先要求模式串的失效函数next,然后再根据next的值进行模式匹配,在最坏情况下的时间复杂度为O(m*n),m为模式串的长度,n为主串的长度,当如果模式串中有较多相同的字符时,时间复杂度一般可达到O(m+n)。
蒙特卡罗(MonteCarlo)算法
MonteCarlo随机算法主要是通过比较模式串和主串中与模式串等长的串的“指纹”来匹配的,若两者指纹相等,则可以认为在概率意义下两者是相等的,算法中要求用到一个随机产生的素数作模运算,该素数的选取直接影响了算法的准确率,算法的时间复杂度为O(m+n)。但有一定的出错率,即选取主串中比较串的指纹与模式串相等时但比较串与模式串并不相等,理论上这种情况出现的概率为1/n,只与主串的长度有关,与模式串的长度无关,但实际上只要选取素数合适出错率比1/n要小的多。
拉斯维哥斯(LasVegas)算法
LasVegas算法是对MonteCarlo算法的改进,当模式串的指纹与主串中的比较串相等时,此时并不直接返回匹配的位置,而是判断两个字符串是否真的相等,相等则返回,否则继续匹配。所以,该算法总能得到正确的位置,但算法的执行时间稍微比MonteCarlo算法要长一点(判断字符串是否相等的耗费),时间复杂度的期望值不超过O(m+n)。
三种算法的实现和分析
要完成上述三个模式匹配算法的比较,需要一个0/1串的随机发生器和一个素数发生器。程序中头文件”randstr.h”包含的RandomString类是用来产生MAXSIZE对的主串和模式串的,0/1串的长度和内容都是随机的,为了便于比较,规定主串的长度一定大于模式串的长度。”random.h”包含的Random类封装了产生随机数的函数。素数发生器首先产生MAXSIZE个随机数保存在prime数组中,供随机算法使用。本程序中随机生成了10000对0/1串作为测试数据,即MAXSIZE=10000。”defs.h”定义了所用的常量。
运行PatternMatching可以发现:
1) 三个算法运行的时间处于同一数量级的,其中在大多数情况下MonteCarlo的算法都要快于KMP和LasVegas算法,从理论上分析MonteCarlo算法的平均时间复杂度优于KMP算法,一般情况MonteCarlo时间复杂度为O(m+n),而KMP最好情况O(m+n),最坏为O(n*m)。LasVegs要比MonteCarlo稍微慢一点点,这是预料之中的,因为判断字符串相等耗费了额外的时间。KMP和LasVegs算法的平均时间复杂度大致相等。
2) 随机选取的素数大小直接影响了MonteCarlo算法的出错率。在模式串不是很长时,当素数大于某个数时我们可以发现出错率可以降到0。设模式串的最长长度为m,当随机产生的素数p>2m时, Y和X(j)的对p作模运算后的“指纹”Ip都要小于p, 此时p不可能可以整除|Y − X(j)|,因此不会出现当Ip(X(j))=Ip(y)时却有X(j)≠Y的误判情况,所以这种情况下MonteCarlo出错率为0。
3) 素数一定大时,MonteCarlo算法的出错率比理论值1/n要小的多,即当Ip(X(j))= Ip(y)时却有X(j)≠Y的情况很少。相反,当素数很小时,不同0/1序列对素数作模运算的结果相同的可能性增大,出错率相应地变大。
4) 当模式串的长度比主串小很多时,三个算法的执行时间明显快了,KMP和MonteCarlo算法的执行时间几乎相等。这也是比较容易理解的,模式串很短意味着它与主串匹配成功的可能性就大,算法不需要从头到尾扫描一遍主串就可以在主串的较前面找到匹配串的位置,此外,模式串的长度小则说明耗费在扫描一遍模式串的时间就短,因此执行算法所花费的时间就少得多,KMP时间复杂度接近O(m+n),与MonteCarlo算法相等。
- //文件名:PatternMatching.cpp
- //功能:实现并比较三种模式匹配算法:KMP,MonteCarlo,LasVegas
- #include "random.h"
- #include "randstr.h"
- #include "defs.h"
- #include <iostream>
- #include <fstream>
- #include <time.h>
- #include <windows.h>
- #include <stdlib.h>
- #include <math.h>
- #include <string.h>
- using namespace std;
- //////////////////////////函数声明///////////////////////////////////////////////////////////////////////////////
- bool isprime(int n);//判断n是否为素数
- int random_prime( int min, int max ); //随机产生一个min~max-1之间的素数
- int KMP(char* s, char* t, int position); //KMP算法
- int getIP(char *x, int len, int prime); //获取x[0]…x[len-1]的指纹
- int MonteCarlo(char* x, char* y, int position, int prime);//MonteCarlo算法
- int LasVegas(char* x, char* y, int position, int prime); //LasVegas算法
- //////////////////////////函数定义//////////////////////////////////////////////////////////////////////////////
- //函数名:isprime
- //功能:测试一个整数是否为素数
- //输出:若n为素数,则返回true;否则false
- bool isprime(int n)
- {
- for(int i = 2; i <= sqrt((double)n); i++)
- if(n % i == 0)return false;
- return true;
- }
- //函数名:random_prime
- //功能:随机产生一个[min, max-1]区间上的素数
- int random_prime( int min, int max )
- {
- int i;
- srand((int)time(0));
- i = rand() % (max - min) + min;
- for(; i >= min; i--)
- if(isprime(i)) break;
- return i;
- }
- //////////////////////三种模式匹配算法的实现///////////////////////////
- //I、KMP算法
- //函数名:KMP
- //功能:利用KMP算法匹配模式串
- //输入:主串s和模式串t
- //输出:模式串在主串第pos个字符之后出现的位置
- int KMP(char* s, char* t, int pos)
- {
- int i, j;
- int s_len = (int)strlen(s);
- int t_len = (int)strlen(t);
- //先求模式串t的next
- int *next = new int[t_len];
- i = 0;
- j = -1;
- next[0] = -1;
- while(i < t_len)
- {
- if(j == -1 ) { i++; j++; next[i] = j ; }
- else
- {
- if(t[i] == t[j]) { i++; j++; next[i] = j ;}
- else j = next[j];
- }
- }
- //再匹配模式串,求在主串中的位置
- i = pos - 1 ;
- j = 0;
- while(i < s_len && j < t_len)
- {
- if( j == -1 || s[i] == t[j]) { i++; j++; }//继续比较后面的字符
- else j = next[j];//模式串向右移动
- };
- //delete next;
- if(j >= t_len) return (i - t_len) + 1;
- else return 0;
- }
- //II、MonteCarlo算法
- //函数名:getIP
- //功能:求序列x的指纹
- //输入:01串x,长度len
- //输出:X(i) = x[i]x[i+1]...x[i+len-1] mod p的指纹
- int getIP(char *x, int len, int p)
- {
- int ip = 0;
- for(int k = 0 ; k <= len -1; k++)
- ip = ( 2 * ip + x[k] - '0') % p;
- return ip;
- }
- //函数名:MonteCarlo
- //功能:利用随机算法MonteCarlo进行模式匹配
- //输入:主串s和模式串t,,随机素数p
- //输出:模式串在主串第pos个字符之后出现的位置
- int MonteCarlo(char* x, char* y, int pos, int p)
- {
- int j = 0;
- int Ipx, Ipy, wp;
- int x_len = (int)strlen(x);
- int y_len = (int)strlen(y);
- //取指纹
- Ipx = getIP(x + pos - 1, y_len, p);
- Ipy = getIP(y, y_len, p);
- //计算2m mod p
- wp = 1;
- for(int i = 0; i < y_len; i++)
- wp = (wp * 2) % p;
- //开始匹配模式串
- while( j <= x_len - y_len - pos + 1)
- {
- if(Ipx == Ipy) return j + 1;
- else
- {
- Ipx = ( 2 * Ipx - wp * ( x[j] - '0' ) + (x[j + y_len] - '0') ) % p;
- if(Ipx < 0) Ipx += p;
- if(Ipx >= p) Ipx -= p;
- j++;
- }
- }
- return 0;
- }
- //III、LasVegas算法
- //函数名:LasVegas
- //功能:对MonteCarlo算法的改进,当Ip(X(j))=Ip(Y)时比较X(j)与Y是否相等,若相等则返回
- // 子串在主串中的位置,否则继续执行循环。该算法总能给出正确的位置
- //输入:主串s和模式串t,,随机素数p
- //输出:模式串在主串第pos个字符之后出现的位置
- int LasVegas(char* x, char* y, int pos, int p)
- {
- int j = 0;
- int Ipx, Ipy, wp;
- int x_len = (int)strlen(x);
- int y_len = (int)strlen(y);
- //取指纹
- Ipx = getIP(x + pos - 1, y_len, p);
- Ipy = getIP(y, y_len, p);
- //计算2m mod p
- wp = 1;
- for(int i = 0; i < y_len; i++)
- wp = (wp * 2) % p;
- //开始匹配模式串
- while( j <= x_len - y_len -( pos - 1) )
- {
- if(Ipx == Ipy && !strncmp(x + j, y, strlen(y)))
- return j + 1;
- else
- {
- Ipx = ( 2 * Ipx - wp * ( x[j] - '0' ) + (x[j + y_len] - '0') ) % p;
- if(Ipx < 0) Ipx += p;
- if(Ipx >= p) Ipx -= p;
- j++;
- }
- }
- return 0;
- }
- ////////////////////////////主函数////////////////////////////////////////////////////////////////////////
- main()
- {
- char *x, *y;
- DWORD t_start, t_end;//记录算法开始执行时间和结束时间
- int m,n;
- int index_KMP[MAXSIZE], index_MC[MAXSIZE], index_LV[MAXSIZE];
- //分别存放KMP和MonteCarlo算法返回的匹配结果,即模式串在主串中首次出现的位置
- int prime[MAXSIZE]; //存放MAXSIZE个随机产生的素数
- int minlen, maxlen;
- while(1)
- {
- cout << " 主串的最长长度: ";
- cin >> maxlen;
- cout << " 子串的最长长度: ";
- cin >> minlen;
- //随机产生MAXSIZE对主串和子串最长长度分别为maxlen,minlen的0/1串
- RandomString rs(minlen,maxlen);
- //素数发生器:产生MonteCarlo和Las Vegas算法中所需要的素数
- for(int i = 0; i < MAXSIZE; i++)
- {
- x = rs.getX(i);
- y = rs.getY(i);
- n = (int)strlen(y);
- m = (int)strlen(y);
- prime[i] = random_prime(m*m,MAXINT32);//随机产生一个素数
- }
- cout << endl;
- cout << " 三种模式匹配算法运行时间:"<< endl;
- cout << " -----------------------------" << endl;
- //KMP算法
- t_start = GetTickCount();//开始时间
- for(int i = 0; i < MAXSIZE; i++)
- {
- x = rs.getX(i);
- y = rs.getY(i);
- index_KMP[i] = KMP(x, y, 1);
- }
- t_end = GetTickCount();//结束时间
- cout << " KMP :" << t_end - t_start << " ms" << endl;
- //MonteCarlo算法
- int mismatch = 0 ; //失败的次数
- t_start = GetTickCount();//开始执行MonteCarlo算法
- for(int i = 0; i < MAXSIZE; i++)
- {
- x = rs.getX(i);
- y = rs.getY(i);
- index_MC[i] = MonteCarlo(x, y, 1, prime[i]);
- }
- t_end = GetTickCount();//MonteCarlo算法运行完毕
- cout << " MonteCarlo :" << t_end - t_start << " ms" << endl;
- //LasVegas算法
- t_start = GetTickCount();//开始执行MonteCarlo算法
- for(int i = 0; i < MAXSIZE; i++)
- {
- x = rs.getX(i);
- y = rs.getY(i);
- index_LV[i] = LasVegas(x, y, 1, prime[i]);
- }
- t_end = GetTickCount();//LasVegas算法运行完毕
- cout << " LasVegas :" << t_end - t_start << " ms" << endl;
- //将MonteCarlo算法得到的结构与KMP算法得到的结果相比较,统计其失败率
- for(int i = 0; i < MAXSIZE; i++)
- {
- if(index_MC[i] != index_KMP[i]) mismatch ++;
- }
- cout << " -----------------------------" << endl;
- double failure = (double)mismatch/MAXSIZE;
- cout << " MonteCarlo算法的出错率: " << failure * 100 << "%"<< endl;
- cout << endl;
- }
- delete x;
- delete y;
- }
实验数据
为了更好的说明问题,对p的大小和模式串的长度选取了几组不同的测试数据,以下为不同数据的运行结果(其中m,n分别为主串和模式串的长度,m, n, p都是在给定的区间上随机取值):
1)第一组:素数p<2^m,取m=[1,16], n=[m,128],p=[1, 2^m],从测试结果可以看出MonteCarlo算法的出错率达到了20%以上,这是难以接受的。p越小MonteCarlo算法的出错率越大。主串的最长长度为128,子串的最长长度为16.
KMP | 16ms |
MonteCarlo | 16ms |
Lavegas | 31ms |
MonterCarlo算法的出错率 | 22.4% |
2)第二组:取m=[1,16], n=[m,128], p=[2,2^16],此时随机选取素数p既有可能小于也有可能大于,MonteCarlo算法的出错率只有0.03%.主串的最长长度为128,子串的最长长度为16
KMP | 16ms |
MonteCarlo | 16ms |
Lavegas | 31ms |
MonterCarlo算法的出错率 | 0.03% |
KMP | 31ms |
MonteCarlo | 16ms |
Lavegas | 31ms |
MonterCarlo算法的出错率 | 0% |
KMP | 16ms |
MonteCarlo | 16ms |
Lavegas | 31ms |
MonterCarlo算法的出错率 | 0% |
5)第五组:取m=[1,2000], n=[m,2048], p=[2^16,2^32],素数p取介于16位机器表示最大整型数和32位机器表示最大整型数之间,此时MonteCarlo算法的出错率为0.07% < 1/n。主串的最长长度为2048,子串的最长长度为2000.
KMP | 625ms |
MonteCarlo | 1266ms |
Lavegas | 1297ms |
MonterCarlo算法的出错率 | 0.07% |