零零散散学算法之多串匹配

多字符串匹配

第一节 提出问题

        所谓多串匹配,就是给定一些模式串(子串),在一段正文(主串)中找到第一个出现的任意一个模式串的位置。具体来说就是:给定m个长度分别为L1、L2......Lm的模式串数组A[1..L1]、A[1..L2]......A[1..Ln],假设主串为一个长为n的数组T[1..n],那么在主串中的某一位置X,对于满足匹配的任意串Y,会满足A[1..LY]∈T[X...X+Y]。

        假如模式串分别为superfc与king,正文为kingsuperfc,那么此时在主串(正文)中即可找到子串(模式串)。那么,我们如何解决这个问题呢?最简单的做法就是从小到大枚举每一个位置,并进行检查。该算法最坏情况下时间复杂度为O(n*Lx)。

枚举法:
int i = 0, j = 0;
for(;i < n;i++)
{
	for(;j < m;j++)
	{
		if(i + Lj - 1 < n)
		{
			if(T[i..i+Lj-1] == Aj[1...Lj])
			{
				/***输出位置i
				 ***并判断子串是否已经匹配完,如果已匹配完,那么break
				 ***/
			}
			else
			{
				j = 0;
				break;
			}
		}
	}
}

        不过这种方法在解决我们将要处理的数据量时很难达到我们预期的要求于是我们应该想出一种更优秀的算法来 处理该问题。为此,我们下面将介绍KMP(Knuth-Morris-Pratt)算法、单词前缀树算法,以及后缀树算法。

第二节 KMP算

        定义:给定一个长度为m的模式串A[1..m],以及一个长度为n的正文T[1..n],需要从主串T中找到子串A出现的位置的引。

        模式串的前缀函数(prefix Function)我们知道普通的字符串匹配算法都要回溯,而KMP不需要回溯。那么当主串本身有前后“部分匹配”的情况时怎么办呢?这时模式串的前缀函数就派上用场了。


该函数可以通过下面的程序来实现:
/***next[] function:***/
k = 0;
next[k] = 0;
for(i = 1;i < m;i++)
{
	while(k > 0 && p[k] != p[i])
		k = next[k];
	if(p[k] == p[i])
		k = k + 1;
	next[i] = k;
}

       上面的代码中,k的增加值最多为m-1(k = k + 1),执行的次数不会超过m-1次。所以该段程序的复杂度为(m)。
因为前缀函数记录了模式串自身的唯一匹配情况,所以在串匹配算法中,遇到了不匹配的字符,就可以根据前缀函数进行适当的调整,而不需要回溯来重新对比了。

匹配的过程如下:



与求前缀数组的算法类似,我们可得出主算法(求主算法的过程略过)。

第三节 单词前缀树

       所谓单词前缀树,其实就是一颗单词查找树。理想状况下单词树是一棵无限延伸的26叉树。每个节点均是通向26个子节点的边,被分别命名为a,b,c......z。

       对于每一个节点p,从根节点到p的路径上的所有字母,可以连成一个字符串,我们定义这个字符串为Sp。相反,对于一字符串S,我们可根据字符串上的字母,确定该字母在单词树中的相应位置,以及确定此路径上的尾节点。由此可得,单词树上的节点,与字符串之间的关系是一一对应。 那么单词树是怎样生成的呢?很简单,只需在一颗初始状态为空的树中不断地插入即可。如下图:

所以,对于一个长度为N的字符串S[N]的插入过程如下实现:
p = root;
for(i = 0;i < N;i++)
{
	if(p->WordTree[S[i]] == NULL)
		malloc(p->WordTree[S[i]]);//之后再对其初始化
	p = p->WordTree[S[i]];
}

       ok,这时就有一个问题了。单词前缀树与单词树有什么不同呢?不同之处在于单词前缀树的每一个非根节点上都有一个前缀指针。但是这个前缀指针是怎样生成的呢?我们可效仿KMP算法中的前缀函数(可见,单词前缀树的时间复杂度是和KMP算法相关的),通过其父节点的前缀指针,找到当前节点的前缀指针。定义一个节点的前缀指针所指向的节点为该节点的前缀节点。令当前节点为p,此时深度比p小的节点的前缀指针均已实现。此操作过程我们可以如下来代码完成:
q = p->FatherNode;
char ch = q->p;
q = q->prefix;

while(q != root && q->WordTree[ch] == NULL)
	q = q->prefix;
	
if(q->WordTree[ch] == NULL)
	p = p->root;
else
	p->prefix = q->WordTree[ch];
/***其中p->prefix代表p节点的前缀指针***/

对于以上的讲解,我search了一例子来做说明。如下图:

       我们在球节点p的前缀指针时,首先找到p节点的父节点的前缀节点q1,但是从图中看q1没有标记为字符b的通向子节点的边,因此只能再找q1的前缀节点,即q2。这次q2有一条通向q3的标记为b的边,由此可得q3为p节点的前缀节点。

       好了,至此我们利用单词前缀树的多串匹配算法也就成了。代码如下:
int MultiCharMatch()
{
	首先建立单词前缀树;
	q = root;
	for(i = 0;i < n;i++)
	{
		while(q != root && q->WordTree[S[i]] == NULL)
			q = q->prefix;
		if(q->WordTree[S[i]] != NULL)
			q = q->WordTree[S[i]];
		此时,判断q是否出现在模式串中即可!
	}
	return 0;
}

第四节 后缀树和McCreight算法

       后缀树提出的目的是用来有效地字符串匹配和查询。它的基本结构,是由一个字符串的后缀组成的单词树(关于单词树,在前面已介绍)。如对于字符串“ababc”,见下图:

       这五个节点分别代表了五种不同的后缀,由图中看出出现的节点数的数量级为O(N*N),这样的话势必会影响程序的执行效率。于是,我们对其进行了改进------路径的压缩,如下图所示:

       经过压缩之后每个节点最多有26个子节点,并且通向子节点的边上的字符串的首字母各不相同。这样一来,节点数个数的数量级就成了O(N)。

       说到后缀树,就不得不提一下McCreight 算法。
附注 下两段内容来自ljsspace

       McCreight 算法:简称mcc算法,是基于蛮力法。即已知输入文本串T的内容,逐步缩短插入到树中的后缀长度,直到将最后一个后缀(末尾的那个字符)插入到前面已经生成的树中为止。McCreight算法的核心思想是suffix link(后缀连接)和head/tail的概念。所谓结点X的suffix link指向的结点Y,指的是如果从根结点出发到X结点终止时的字符串等于xW(其中小写字母x表示单个字符,W表示一个字符串),那么从根结点出发到Y结点终止时的字符串等于W。head[i]指的是后缀树T中,Suffix[i]与所有后缀共享的前缀中最长的前缀。

       如下图,McCreight算法基本流程可以描述为:
       第一步:这里树的左枝对应插入后缀Suffix[i-1]之后的效果,v和u都是内部结点,其中v是head[i-1]对应的内部结点,u是该树枝中v的上一个内部结点(u最接近v,也可以等于v)。
       第二步:在插入Suffix[i]时,先沿着u的suffix link(因为在插入完Suffix[i-1]之后,除了v可能没有suffix link外,其余的内部结点都有suffix link - 这一点可以用归纳法证明),找到树T[i-1]中的内部结点s(u)(注意:suffix link指向的结点一定都是内部结点)。
       第三步:这时开始进行插入Suffix[i]的操作,接下来分两小步完成插入第i个后缀。
              第一小步:使用快速扫描(fast scan),沿着s(u)结点往树叶方向搜索,直到找到w结点为止,这个w结点就        是v的suffix link应该指向的结点(但是此时很有可能这个suffix link还不存在),建立v到w的suffix link;
              第二小步:在找到w的基础上,使用慢速扫描(slow scan),即沿着w结点往树叶方向搜索,直到找到head[i]          为止。这时就可以结束插入Suffix[i]的工作。
需要注意,在fastscan和slowscan中都需要记录u'的结点位置,这样在插入下一个后缀Suffix[i+1]时可以快速jump到s(u'),从结点s(u')开始,而不需要像蛮力法那样从root结点去搜索,这就是为什么mcc算法能够达到O(n)线性复杂度的原因。


第五节   结束

转载请标明出处,原文地址:http://blog.csdn.net/fengchaokobe/article/details/7404247

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值