从病毒感染检测谈串的模式匹配

病毒感染检测:串的模式匹配 BF、KMP算法

在网络入侵检测、计算机病毒特征码匹配以及DNA序列匹配等应用中都需要进行串的模式匹配。针对这个问题,书上讲了两种模式匹配的算法,即BF算法和KMP算法,下面针对这两种算法的实现谈谈我的思路。

1、代码部分

BF算法

#include
using namespace std;
#include

//采用静态顺序存储结构(定长) 
typedef struct{
	char ch[1000001];	//存储串的一维数组 
	int length;		//串的长度 
}SString;
 
 SString S,T; 
 
 char s[1000001];
 char t[1000001];
 
//BF算法
//查找 模式T 在 主串S 中第pos个字符开始第一次出现的位置,并返回 
//若不存在,则返回0 (T非空,1<=pos<=S.length) 
int Index_BF(SString S,SString T,int pos)  
{				
	int i,j;
	i=pos;
	j=0;
	while(i<=S.length-1 && j<=T.length-1)
	{
		if(S.ch[i]==T.ch[j]){	//从各自的第一位开始比较,如果相同,比较下一位 
			++i;
			++j;
		}
		else {//如果不同,主串指针回到 上次开始比较时的字符 的下一个字符,
			  //模式回到第一个字符,重新开始比较 
			i=i-j+1;
			j=0;
		} 
	}
	if(j>T.length-1)		//匹配成功 
		return i-T.length+1;//主串指针位置往回退模式长度个单位,就回到了该模式在主串中第一次出现的位置 
	else			//匹配失败 
		return 0;	//返回0(顺序存储的字符串是从下标为1的数组分量开始存储的,下标为0的分量闲置不用) 
} 
 
//主函数 
int main()
{
	cin>>s>>t;
	strcpy(S.ch,s);
	strcpy(T.ch,t);
	S.length=strlen(S.ch);
	T.length=strlen(T.ch);
    cout<<Index_BF(S,T,0)<<endl;;
	return 0;
}

KMP算法

#include
using namespace std;
#include

//采用静态顺序存储结构(定长) 
typedef struct{
	char ch[1000002];	//存储串的一维数组 
	int length;		//串的长度 
}SString;
 
 SString S,T; 
 
 char s[1000002];
 char t[1000002];
 int nex[1000002];
 
//KMP算法
//查找 模式T 在 主串S 中第pos个字符开始第一次出现的位置,并返回 
//若不存在,则返回0 (T非空,1<=pos<=S.length) 
int Index_KMP(SString S,SString T,int next[])  
{				
	int i,j;
	i=j=0;
	while(i<=S.length-1 && j<=T.length-1)
	{
		if(j==-1||S.ch[i]==T.ch[j]){	//从各自的第一位开始比较,如果相同,比较下一位 
			++i;
			++j;
		}
		else {
			j=next[j];
		} 
	}
	if(j>T.length-1)		//匹配成功 
		return i-T.length+1;// 
	else			//匹配失败 
		return 0;
} 
 
 void get_next(SString T,int next[]){
 	int i=0;
 	next[0]=-1;
 	int j=-1;
 	while(i<T.length-1){
 		if(j==-1||T.ch[i]==T.ch[j]){
 			++i;//前缀开始的位置
 			++j;//后缀开始的位置
 			next[i]=j;//next[]是我的子串和主串的那一位对着的位置,现在把后缀开始的位置值赋给他。
		 }
		 else {
		 	j=next[j];
		 }
	 }
 }
//主函数 
int main()
{
	cin>>s>>t;
	strcpy(S.ch,s);
	strcpy(T.ch,t);
	S.length=strlen(S.ch);
	T.length=strlen(T.ch);
	get_next(T,nex);
    cout<<Index_KMP(S,T,nex)<<endl;
	return 0;
}

病毒感染检测

#include
#include

using namespace std;

int Index_BF(string, string, int); //匹配则返回开始下标,否则返回-1 
int Virus_detecion(string, string); //感染则返回1,否则返回0

int main()
{
	string s,t;
	cin >> s >> t;
	if(Virus_detecion(s, t)==1)	cout << "YES" << endl;
	else cout << "NO" << endl;
	return 0;	
}

int Index_BF(string s, string t, int pos)
{//返回模式t在主串s中第pos个字符开始第一次出现的位置下标。
//若不存在,则返回值为-1 
 //其中,t非空,1≤pos≤StrLength(s) 
	int i,j;
   	i = pos-1; //下标 
	j = 0; //下标 
   	while(i<s.length() && j<t.length()){ 
      	if(s[i]==t[j]){++i; ++j;}	//继续比较后继字符 
      	else{i=i-j+1; j=0;}	//指针后退重新开始匹配
   }   
   if(j==t.length()) return i-t.length(); //模式串全部读完,表示匹配,返回开始匹配位置的下标 
   else return -1;   
} 

int Virus_detecion(string s, string t)
{//求病毒DNA(环状)在人DNA中是否出现,有则返回在人DNA中第几个字符出现,无则返回0 
	int num = t.length();
	t += t; //模式串生成两次	
	string temp; 
	int res;
	for (int i=0; i<num; i++){
		temp.assign(t, i, num); 		
		//cout << temp << endl;
		res = Index_BF(s, temp, 1) ; //调用 Index_BF 检测本次的num个字符是否与s匹配 
		if(res!=-1)  return 1; // 本次匹配,说明感染病毒 
	}
	
	return 0; //所有检测都不匹配,说明没有感染病毒 
}



2、探索过程

一、BF算法

【错误代码1】

#include
#include
using namespace std;
 typedef struct{
 	char ch[1000002];
 	int length;
 }SString;


void  Index_BF(SString S, SString T, int pos){ 
    int i,j;
	i=pos;
	j=1;
    while ( i <= S.length && j <= T.length ) {
       if ( S.ch[i]==T.ch[j] ) { ++i;  ++j; }
       else { i = i-j+2;  j=1; }
     if ( j>T.length )   
	 {
	 	cout<< i-T.length<<endl;
	 }
     else{ 
	 cout<<"0"<<endl;
	} 
}
}
int main()
{
	SString S,T;
	char s[1000002]={0},t[1000002]={0};
	cin>>s>>t;
	strcpy(S.ch,s);
	strcpy(T.ch,t);
	Index_BF(S, T , 1);
	return 0;
}

分析:编译轻松通过,但是怎么都不能输入进去。参考其他同学的博客,发现我的这个问题有人也遇到过,原因在于在主函数中定义了两个长度为100W的数组,程序根本跑不动。
解决:参考博客发现有两种途径解决数组过大问题:①将数组定义为全局变量 ②动态分配数组
针对我这一题的具体情况,因为我是要用到strcpy函数来copy数组的内容的,所以第一种方式显然更适合。


【错误代码2】

#include
#include
using namespace std;
 typedef struct{
 	char ch[1000002];
 	int length;
 }SString;
	char s[1000002]={0},t[1000002]={0};
	SString S,T;
	
void  Index_BF(SString S, SString T, int pos){ 
    int i,j;
	i=pos;
	j=1;
	cout<<"smooth"<<endl;
    while ( i <= S.length && j <= T.length ) {
       if ( S.ch[i]==T.ch[j] ) { 
	   ++i; 
	   ++j; }
       else {
	    i = i-j+2;
	     j=1; }
     if ( j>T.length )   
	 {
	 	cout<< i-T.length<<endl;
	 }
     else{ 
	 cout<<"0"<<endl;
	} 
}
}
int main()
{
	cin>>s>>t;
	cout<<"good"<<endl;
	strcpy(S.ch,s);
	cout<<"OK"<<endl;
	strcpy(T.ch,t);
	cout<<"OK"<<endl;
	Index_BF(S, T , 1);
	return 0;
}

分析:这一次程序是顺利运行完了,就是没有想要的结果出来。
解决:为了查找程序在哪里断掉了,我在一些步骤之后设置了一些输出,比如cout<<“good”。此法说明我的问题出在BF算法上。经过仔仔细细的检查,发现BF算法中我打while循环的时候掉了一个括号,加上去就正常了。
接下来需要解决位置和下标的问题,我现在输出的还是下标,不是题目要求的位置。思考过后发现解决的途径有两种:改数组;改算法。这里我选择的是改算法的方法。


【正确程序1】

#include
using namespace std;
#include

//采用静态顺序存储结构(定长) 
typedef struct{
	char ch[1000001];	//存储串的一维数组 
	int length;		//串的长度 
}SString;
 
 SString S,T; 
 
 char s[1000001];
 char t[1000001];
 
//BF算法
//查找 模式T 在 主串S 中第pos个字符开始第一次出现的位置,并返回 
//若不存在,则返回0 (T非空,1<=pos<=S.length) 
int Index_BF(SString S,SString T,int pos)  
{				
	int i,j;
	i=pos;
	j=0;
	while(i<=S.length-1 && j<=T.length-1)
	{
		if(S.ch[i]==T.ch[j]){	//从各自的第一位开始比较,如果相同,比较下一位 
			++i;
			++j;
		}
		else {//如果不同,主串指针回到 上次开始比较时的字符 的下一个字符,
			  //模式回到第一个字符,重新开始比较 
			i=i-j+1;
			j=0;
		} 
	}
	if(j>T.length-1)		//匹配成功 
		return i-T.length+1;//主串指针位置往回退模式长度个单位,就回到了该模式在主串中第一次出现的位置 
	else			//匹配失败 
		return 0;	//返回0(顺序存储的字符串是从下标为1的数组分量开始存储的,下标为0的分量闲置不用) 
} 
 
//主函数 
int main()
{
	cin>>s>>t;
	strcpy(S.ch,s);
	strcpy(T.ch,t);
	S.length=strlen(S.ch);
	T.length=strlen(T.ch);
    cout<<Index_BF(S,T,0)<<endl;;
	return 0;
}

现在把程序放到PTA上面跑,得到了15分,唯一错误的一个点就是超时。但这个是BF算法无法解决的了,所以我决定一鼓作气把他改成KMP算法。

二、KMP算法的探索

在真正着手写代码前,我花了很久阅读课本,也想了很久,终于弄明白了这个算法的核心思想。以下是我的思路:

①BF究竟是哪里麻烦了?

设想一种情况:主串是abcab... 子串是abcac... 如果按照BF算法我们知道,一旦b和c不匹配了,接下来比较的就是主串的第二个字符b和子串的第一个字符a,不匹配时主串和子串都右移。直到子串的首位字符a与主串的a对齐之前,我们做的都是无用功。这就是从一个具体的例子看出来的麻烦之处。

②如果说KMP可以跳过这些“无用功”,那什么情况下可以跳?这是一个特殊情况,真的能上升到一个算法的高度吗?

为了解决我以上的疑虑,我决定用抽象的模型进行说明。

设主串Si(大家不要把它看成硅元素的元素符号啦)与模式tj失配……这句话暗含(S1S2……Si-1与t1t2……tj-1是匹配的) 这里我们将主串Si固定死,让他与模式中的tk匹配(因为不知道,所以设k这个未知数,也就是说这时候模式要从j跳到k)即:

Si-k+1...Si-1 =t1...tk-1

我们由第一步的匹配就已经得到:

Si-k+1...Si-1 = tj-k+1...tj-1

把这两个式子联立,得到:
t1...tk-1 = tj-k+1...tj-1

③求k

以上我们得到了一个很重要的关系式,即t1...tk-1 = tj-k+1...tj-1,它隐含了我们想要知道的k,也就是子串该跳到哪一个地方这一信息。

观察式子,我们首先考虑一些边边角角的问题,比如k=1,j=1这些情况。

1)k=1 这时式子变成了t1...t0 = tj...tj-1 这种情况说明子串不符合要求,没有那个相等的部分。顺便也解决了我们第二问中的疑惑,其实KMP算法的优化是有条件的,要求子串与主串匹配的那部分有“相等的部分”。 这时候的处理办法也只有老老实实跳到t1与Si比较。模式啊模式!你若是堕落,KMP也救不了你!

2)j=1 其实就是模式的第一位就与主串Si不匹配。那还犹豫啥?直接把模式向右移一个呗。

④怎么用算法实现?

其实整个大框架在BF算法的基础上来说不用怎么改,只需要把回溯的那块儿稍微修改一下,即改成主串i不回溯,模式跳到k位置。

但是现在说来说去我们还是只有一串关系式,还有两个边界的情况,k到底怎么表示?对解决这个问题还是一脸懵逼。

阅读课本发现他使用一个next[j]函数来表示子串下一个回溯的位置。 其实想一想next[j]仅与模式有关,和主串半毛钱关系也没有。从已知的关系式t1...tk-1 = tj-k+1...tj-1入手,此时next[j] = k。我们求一求next[j+1]?这时候产生两种情况需要讨论:

1)若tk = tj 那么t1...tk = tj-k+1...tj 也就是next[j+1] = k+1 , 亦即next[j+1] = next[j] +1

2)若tk!=tj 是不是有一种似曾相识的感觉?对了,这又是一次模式匹配,只不过此时的模式既充当了主串又充当模式。所以我们要把模式中的第next[j]个字符和主串中的第j个字符对齐进行比较。这种情况可以利用函数的递归调用,让j=next[j]。


【正确程序2】

#include
using namespace std;
#include

//采用静态顺序存储结构(定长) 
typedef struct{
	char ch[1000002];	//存储串的一维数组 
	int length;		//串的长度 
}SString;
 
 SString S,T; 
 
 char s[1000002];
 char t[1000002];
 int nex[1000002];
 
//KMP算法
//查找 模式T 在 主串S 中第pos个字符开始第一次出现的位置,并返回 
//若不存在,则返回0 (T非空,1<=pos<=S.length) 
int Index_KMP(SString S,SString T,int next[])  
{				
	int i,j;
	i=j=0;
	while(i<=S.length-1 && j<=T.length-1)
	{
		if(j==-1||S.ch[i]==T.ch[j]){	//从各自的第一位开始比较,如果相同,比较下一位 
			++i;
			++j;
		}
		else {
			j=next[j];
		} 
	}
	if(j>T.length-1)		//匹配成功 
		return i-T.length+1;// 
	else			//匹配失败 
		return 0;
} 
 
 void get_next(SString T,int next[]){
 	int i=0;
 	next[0]=-1;
 	int j=-1;
 	while(i<T.length-1){
 		if(j==-1||T.ch[i]==T.ch[j]){
 			++i;//前缀开始的位置
 			++j;//后缀开始的位置
 			next[i]=j;//next[]是我的子串和主串的那一位对着的位置,现在把后缀开始的位置值赋给他。
		 }
		 else {
		 	j=next[j];
		 }
	 }
 }
//主函数 
int main()
{
	cin>>s>>t;
	strcpy(S.ch,s);
	strcpy(T.ch,t);
	S.length=strlen(S.ch);
	T.length=strlen(T.ch);
	get_next(T,nex);
    cout<<Index_KMP(S,T,nex)<<endl;
	return 0;
}

三、病毒感染检测

这个问题要处理的操作对象就是字符串,将病毒的DNA序列看作是子串,患者的DNA序列看作是主串,检测任务的实质就是看子串是否在主串中出现过。一定要注意病毒的DNA序列是环状的。而且具体问题中往往不会在输入的窗口中将序列一个个敲上去,往往会将待检测的数据存储到一个文本文件中,输出也以一个文件的形式输出。

由于文件无法直接上传,请大家自行建立一个名为“病毒感染检测输入数据.txt”的文件,内容如下:

11
baa bbaabbba
baa aaabbbba
aabb abceaabb
aabb abaabcea
abcd cdabbbab
abcd cabbbbab
abcde bcdedbda
acc bdedbcda
cde cdcdcdec
cced cdccdcce
bcd aabccdxdxbxa

输出应该是:

baa bbaabbba YES
baa aaabbbba YES
aabb abceaabb YES
aabb abaabcea YES
abcd cdabbbab YES
abcd cabbbbab NO
abcde bcdedbda NO
acc bdedbcda NO
cde cdcdcdec YES
cced cdccdcce YES
bcd aabccdxdxbxa NO

你可能感兴趣的:(数据结构)