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

原创 2012年03月29日 23:08:07
多字符串匹配

第一节 提出问题

        所谓多串匹配,就是给定一些模式串(子串),在一段正文(主串)中找到第一个出现的任意一个模式串的位置。具体来说就是:给定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

相关文章推荐

多模式匹配算法

1. 问题原型:          给定一篇网页,其中有很多敏感词汇或者无效的词,需要找到一种算法,找到这些敏感词。 2. 如何求解呢?    2.1 第一个简单的思路是:    ...
  • beta2
  • beta2
  • 2010年06月27日 22:18
  • 22782

零零散散学算法之详解几种数据存储结构

所谓数据存储结构,就是数据的元素与元素之间在计算机中的一种表示,它的目的是为了解决空间规模问题,或者是通过空间规模问题从而间接地解决时间规模问题。我们知道,随着输入的数据量越来越大,在有限的内存里,不...

零零散散学算法之浅析内存管理的方式

解析内存管理的方式 正文        说到内存分配,我们立刻就会想到malloc()、calloc()等申请内存的接口,说到内存分配的算法,我们会想到Buddy和Slab...

零零散散学算法之详解数据压缩算法(下)

深入解析数据压缩算法   前序               开始本文之前,先回顾一下上篇。上篇讲解了几种数据压缩算法中的两种:Huffman压缩算法和RLE压缩算法。        详解数据...

零零散散学算法之详解几种最短路径

深入解析最短路径算法 正文   第一节 问题的提出及解决方法        所谓最短路径问题,可以说有两种情况来描述。        描述一:在图论中,指的是寻找图中两个节点之间的最短...

零零散散学算法之详解RMQ & LCA

深入理解RMQ & LCA   正文   第一节 RMQ、LCA概述          LCA:Lowest Common Ancestor,译为最近公共祖先。其解释就是说:在有根树中...

零零散散学算法之详解二叉查找树

深入解析二叉查找树 正文        所谓二叉查找树,实质上是按二叉树的结构来组织的,这样的树可以用链表结构来表示,其中每一个节点都是一个对象。        二叉查找树中元素(也...

零零散散学算法之找出数组中重复的数---总结篇

找出数组中重复的数 前序         最近一直在看v_JULY_v的专栏,从中学到了很多关于算法方面的知识,也受到了很大的启发。我相信喜欢算法的朋友,看过他的博文之后也会有这种想法...

IOS开发笔记:IOS的零零散散记录

IOS armv Armv6:iPhone 2G/3G、ipod1G/2G Armv7:iPhone3GS/4/4s、ipod3G/4G、ipad1G/2G/3G Armv7s:iPhone5 U...

Road of Big Data(序)----零零散散的一些说明

关于题目不知道应该怎么写第一篇博客,也不知道要起什么样的名字。。。其实“序”也就是相当于是一个开博说明吧。...
内容举报
返回顶部
收藏助手
不良信息举报
您举报文章:零零散散学算法之多串匹配
举报原因:
原因补充:

(最多只允许输入30个字)