龙空技术网

神奇的高效率字符串匹配KMP算法与病毒感染检测

小智雅汇 571

前言:

现在我们对“字符串模式匹配kmp算法讲解”大致比较关怀,兄弟们都想要知道一些“字符串模式匹配kmp算法讲解”的相关资讯。那么小编在网摘上汇集了一些有关“字符串模式匹配kmp算法讲解””的相关内容,希望大家能喜欢,看官们一起来学习一下吧!

新冠病毒的检测需要采样咽拭子,然后将其中含有的碱基对解螺旋、经过聚合酶链式反应(PCR)扩增后与相应的荧光探针结合,然后进行检测。

而制造能与病毒碱基序列结合的荧光探针,需要知道病毒的碱基序列。

在这个过程中,有一个算法发挥了巨大的作用。

如今的这个算法已经变得十分基础,也有更多的算法超越了它。

但是,它诞生的那一刻,必定闪耀着人类智慧的光芒。

我第一次见到这个算法时,经历了拒绝、疑惑、不解。

直到后来,一切全都化为了惊叹。

这个算法叫KMP算法(Knuth、Morris和Pratt对该算法进行了改进,称为KMP算法)。由于三个字母的原因,有人戏称为“看毛片”算法。但它和“毛片”真的没有一点关系。KMP实际是合力研究出该算法的三位大神的姓名首字母。是的,这个算法是当时三个大神共同努力的结果。

KMP算法实际是一个字符串匹配算法。

子串的定位运算称为串的模式匹配或串匹配。

假设有两个串S,T,设S为主串,也称正文串,T为子串,也称为模式,在主串S中查找与模式T相匹配的子串,如果查找成功,返回匹配的子串第一个字符在主串中的位置。

那它如何为基因测序做出贡献,又为什么让我拒绝、疑惑、不解,最后惊叹。我们一起来重新感受下。

首先,我们先给出算法解决的问题,这是一个极为常见的字符串匹配问题。

存在一个字符串S,请找出其是否含有子串T,如果有,请给出T所在的位置。

假设字符串S的长度为m,字符串T的长度为n。

则见到这个问题后,有经验的程序员不出5秒就能知道其时间复杂度为O(m×n)。是的,这是很多人的第一反应,使用暴力枚举法。

字符串S我们常称为主串,而T则被称为模式串。在主串的任意一个位置,我们都要以此为起点和模式串依次对比。模式串长度为n,而主串中有m个位置,则复杂度为O(m×n)。

#include<iostream>#include<cstring>using namespace std;#define Maxsize 100typedef char SString[Maxsize+1];//0号单元存放串的长度bool StrAssign(SString &T,char *chars)//生成一个其值等于chars的串T{	int i;	if(strlen(chars)>Maxsize)		return false;	else    {		T[0]=strlen(chars);		for(i=1;i<=T[0];i++)        {            T[i]=*(chars+i-1);            cout<<T[i]<<"  ";        }        cout<<endl;		return true;	}}int Index_BF(SString S,SString T,int pos) // BF算法{ 	// 求T在主串S中第pos个字符之后第一次出现的位置	// 其中,T非空,1≤pos≤s[0],s[0]存放S串的长度	int i=pos,j=1,sum=0;	while(i<=S[0]&&j<=T[0])    {        sum++;        if(S[i]==T[j]) // 如果相等,则继续比较后面的字符		{			i++;			j++;		}		else		{			i=i-j+2; // i退回到上一轮开始比较的下一个字符			j=1;     // j退回到第1个字符		}    }	cout<<"一共比较了"<<sum<<"次"<<endl;	if(j>T[0])      // 匹配成功		return i-T[0];	else		return 0;}int main(){	SString S,T;	char str[100];    cout<<"串S:"<<"  ";    cin>>str;        // abaabaabeca	StrAssign(S,str);// 可以修改程序,自己输入字符串     cout<<"串T:"<<"  ";    cin>>str;        // abaabe	StrAssign(T,str);	cout<<endl;	cout<<"BF算法运行结果:"<<endl;	cout<<"主串和子串在第"<<Index_BF(S,T,1)<<"个字符处首次匹配\n";	cout<<endl;	cin.get();cin.get();	return 0;}/* 输入abaabaabecaabaabe*//* 运行效果串S:  abaabaabecaa  b  a  a  b  a  a  b  e  c  a串T:  abaabea  b  a  a  b  eBF算法运行结果:一共比较了15次主串和子串在第4个字符处首次匹配*/

听说有个算法能做到复杂度O(m+n)的时候,听说那个算法叫KMP。

怎么可能呢,O(m+n)意味着比较指针不能回撤,直觉觉得是不可能的。

搜了一下各种资料。

竟然真的是……

如以下字符串匹配:

按暴力枚举法,当元素匹配不成功时,i、j分别要回退:

i=i-j+2; // i退回到上一轮开始比较的下一个字符j=1;     // j退回到第1个字符

是否需要如此回退呢?

看此时能匹配的S、T的公共子串G:abaab。(可以设S的G部分为GS,T的G部分为GT)

因为是依次后移的匹配,可以考虑在GS中的最大后缀(从后向前取若干字符)去匹配GT的前缀(从前向后取若干字符),如以上数据,能匹配的最大子串是ab,ab的长度为L。可以证明,i、j的回退,i不动,j回退到L+1再去比较,算法的正确性不受影响。

而在GS中的最大后缀去匹配GT的前缀,因为GS=GT,也就是在GT中的最大后缀去匹配GT的前缀。也是一个在自身字符串中子串的匹配问题,随着j的不同,如何去求L呢?可以是暴力枚举:

可以用next[j]表示j要回退的位置。

当j=1时,next[j]=0;

当T'的相等前缀和后缀的最大长度为Lmax时,next[j]=Lmax+1;

当没有相等的前缀和后缀时,next[j]=1。

根据以上规则,可以很容易求出T="abaabe“的next数组:

1 2 3 4 5 6 | j

a b a a b e | T

0 1 1 2 2 3 | next[j]

对于回退位置next[]的求解方法,KMP算法应用的却是动态规划的思想:

首先大胆假设next[j]=k,则

那么next[j+1]=?

也就是以上子串再分别多考虑一个随后的字符:

可以区分这两个字符在相等和不等的情况下分别考虑:

回退位置next[]的求解方法的代码实现:

void get_next(SString T,int next[]) //计算next函数值{	int j=1,k=0;	next[1]=0;	while(j<T[0])      // 动态规划	{	    if(k==0 || T[j]==T[k])            next[++j]=++k;		else			k=next[k]; // 下标k的回退	}    cout<<"-----next[]-------"<<endl;    for(j=1;j<=T[0];j++)        cout<<next[j]<<"  ";    cout<<endl;}

KMP算法的代码实现,与暴力枚举相比,仅仅在于i、j的回退位置不同(i不回退)以及求next[j]的动态规划思想:

int Index_KMP(SString S,SString T,int pos,int next[]) // KMP算法{ 	// 利用模式串T的next函数求T在主串S中第pos个字符之后的位置的KMP算法	// 其中,T非空,1≤pos≤StrLength(S)	int i=pos,j=1,sum=0;	while(i<=S[0]&&j<=T[0])    {        sum++;        if(j==0||S[i]==T[j]) // 继续比较后面的字符		{			i++;			j++;		}		else			j=next[j];       // 模式串向右移动    }	cout<<"一共比较了"<<sum<<"次"<<endl;	if(j>T[0])               // 匹配成功		return i-T[0];	else		return 0;}

下面给出一个KMP算法的实现动画,大家关注一下最上面S串的比较指针,S串真的没有任何回撤操作。

KMP算法动画演示:

视频加载中...

等你真正理解了它,发现它并没有太复杂,主要是利用了模式串中的相关性。甚至,会有更优的算法被提出。

void get_next2(SString T,int next[]) // 计算next函数值改进算法{	int j=1,k=0;	next[1]=0;	while(j<T[0])	{	    if(k==0||T[j]==T[k])        {            j++;            k++;            if(T[j]==T[k])                next[j]=next[k];            else                next[j]=k;        }		else			k=next[k];	}    cout<<"-----next[]-------"<<endl;    for(j=1;j<=T[0];j++)        cout<<next[j]<<"  ";    cout<<endl;}

但是KMP算法由D.E.Knuth、J.H.Morris、V.R.Pratt发表于1977。在那个年代,相信KMP算法诞生时,一定闪耀着光芒。

那KMP算法为什么会和基因扯上关系呢?

疫情爆发,专家发现了一种新型环状病毒,这种病毒的DNA序列是环状的,而人类的DNA序列是线性的。专家把人类和病毒的DNA表示为字母组成的字符串序列,如果在某个患者的DNA中发现这种环状病毒,说明该患者已被感染病毒,否则没有感染。

假设病毒的DNA为aabb,患者的DNA为eabbacab,说明该患者已被感染。

如何处理环状病毒呢?

1.环形处理

使用循环存储的方式,循环队列或循环链表。

2.线性处理

将病毒序列扩大两倍,依次从每个下标开始,取m个字符。

算法步骤:

(1) 首先对环状病毒进行处理(环形处理或线性处理)。

(2) 依次把每一个环状病毒变种作为子串,把患者DNA序列作为主串,进行模式匹配。若匹配成功,返回已感染病毒;

(3) 重复运行(2)步。

(4) 如果检测所有病毒变种都未匹配成功,返回未感染病毒。

例如:患者的DNA 序列eabbacab,病毒DNA序列:aabb,检测患者是否感染病毒。

代码实现:

/***病毒检测算法***/#include<cstring>#include<iostream>using namespace std;#define Maxsize 100typedef char SString[Maxsize+1];		//0号单元存放串的长度bool StrAssign(SString &T, char *chars)//生成一个其值等于chars的串T{	int i;	if(strlen(chars)>Maxsize)		return false;	else    {		T[0]=strlen(chars);//0号单元存放串的长度		for(i=1;i<=T[0];i++)        {            T[i]=*(chars+i-1);            cout<<T[i]<<"  ";        }        cout<<endl;		return true;	}}void get_next(SString T,int next[])//计算next函数值{	int j=1,k=0;	next[1]=0;	while(j<T[0])	{	    if(k==0||T[j]==T[k])            next[++j]=++k;		else			k=next[k];	}}int Index_KMP(SString S, SString T, int pos)//KMP算法{ 	// 利用模式串T的next函数求T在主串S中第pos个字符之后的位置的KMP算法	//其中,T非空,1≤pos≤StrLength(S)	int *next=new int[T[0]+1]; // 生成T的next数组	get_next(T,next);	int i=pos, j=1;	while(i<=S[0]&&j<=T[0])    {        if(j==0||S[i]==T[j]) // 继续比较后面的字符		{			i++;			j++;		}		else			j=next[j]; // 模式串向右移动    }	if(j>T[0]) // 匹配成功		return i-T[0];	else		return 0;}bool Virus_detection(SString S, SString T) // 病毒检测{    int i,j;    SString temp;                    // temp记录病毒变种    for(i=T[0]+1,j=1;j<=T[0];i++,j++)// 将T串扩大一倍,T[0]为病毒长度        T[i]=T[j];    for(i=0;i<T[0];i++)        // 依次检测T[0]个病毒变种    {        temp[0]=T[0];          // 病毒变种长度为T[0]        for(j=1;j<=T[0];j++)   // 取出一个病毒变种            temp[j]=T[i+j];        if(Index_KMP(S,temp,1))// 检测到病毒            return 1;    }    return 0;}int main(){	SString S,T;	char str[100];    cout<<"患者DNA序列S:"<<"  ";    cin>>str; // eabbacab	StrAssign(S,str);    cout<<"病毒DNA序列T:"<<"  ";    cin>>str; //aabb	setbuf(stdin,NULL);	StrAssign(T,str);	if(Virus_detection(S,T))        cout<<"该患者已感染病毒!"<<endl;    else        cout<<"该患者未感染病毒!"<<endl;	cin.get();	return 0;}/*输入eabbacabaabb*//*运行效果患者DNA序列S:  eabbacabe  a  b  b  a  c  a  b病毒DNA序列T:  aabba  a  b  b该患者已感染病毒!*/

平时,我们的文本,也就几百上万的字符。即使是做大数据分析,接触到的字符串也不过几十万、上百万个。当然,在这个尺度下,O(m×n)算法和O(m+n)的性能差别已经是极为巨大的。

然而,人类的基因序列,就是腺嘌呤(A)、鸟嘌呤(G)、胞嘧啶(C)、胸腺嘧啶(T)组成的那些ATCGCTACG……等朴实无华的序列串,有30亿个(即30亿对碱基对)。

基因片段的拼接需要不断在上亿的序列(碱基对)中找一个目标序列,O(m×n)算法和O(m+n)的性能差别有万倍、百万倍,甚至更多。

只用暴力匹配的方式,难以完成。

而KMP算法将O(m×n)降到了O(m+n)。只需要读取一遍目标串和模式串,就能给出结果!

在KMP算法诞生的1977年,那时人类才发现DNA的双螺旋结构不过20年,再过大约15年后人类基因组计划才会开始。

当然,在KMP算法诞生后人类基因组计划开始前的15年中,字符串匹配算法可能又会有进步,并且字符串匹配算法也并不最适合基因测序。是新的算法(例如OLC、de Bruijn Graph等算法)用在了基因组计划中。但KMP算法在匹配方面的研究已经打好了基础,它已经提前为人类基因组计划扫除了部分算法上的障碍。

之后,也正因为基因测序技术的发展,人类能迅速锁定出新冠病毒的序列,然后研发测试试剂。帮我们开展测试,推动人类抵御这场突如其来的病毒。

所以,在这场全世界抗击疫情的斗争中,有算法的身影。低调、朴实,但也无可替代。

如今的它,低调,朴实,甚至让人觉着习以为常。

但我知道,它曾经闪耀着人类智慧的光芒;而今,也依旧令人惊艳和伟大。

-End-

标签: #字符串模式匹配kmp算法讲解 #快速精确字符串匹配算法研究方向是什么 #bf算法与kmp算法的异同 #kmp算法用c语言调试成功