热门标签 | HotTags
当前位置:  开发笔记 > 后端 > 正文

【模式匹配】之——多模匹配下篇(AC算法之前缀树实现)

前言本文章对应代码下载地址:http:download.csdn.netdetailsun20434305286986AC算法之前缀树实现步骤步骤一构造前缀树步骤二设置每一个节点的Fai

前言

本文章对应代码下载地址:

http://download.csdn.net/detail/sun2043430/5286986


AC算法之前缀树实现步骤

  1. 步骤一 构造前缀树
  2. 步骤二 设置每一个节点的Failure Node
  3. 步骤三 收集每个节点的所有匹配模式串信息
  4. 步骤四 对目标串进行搜索匹配

AC自动机进行字符串匹配的过程,可以参考维基百科的说明:

http://en.wikipedia.org/wiki/Aho%E2%80%93Corasick_string_matching_algorithm


具体的实现方法就是创建一颗前缀树,根据被查找目标字符串,逐字符匹配目标字符串,从树的根节点往叶子节点一步步查找下去。在这个过程当中,如果失配了,要根据失配跳转点来进行跳转,如果找到匹配的模式串则进行打印输出。这样说还是只有一个模糊的大概印象,我们先看一个简单的例子,然后一步步说明具体的操作步骤。


步骤一:构造前缀树

比如我们现在有5个待搜索模式串:
"uuidi"

"ui"

"idi"

"idk"

"di"

建立如下所示的前缀树:


(图1)

其中,根节点Root为空,不表示任何字符,其ID为0。依次读取每一个模式串,将模式串的每一个字符添加到树上,并依次顺序编号,编号用红色数字显示在节点的右边,每一个叶子节点用黄色背景表示(5,6,9,10,12号节点),表示这里到达一个模式串的结尾(下文称之为尾节点)。如果2个模式串有相同的前缀,则相同的前缀共用相同的节点。例如"uuidi"、"ui"有共同前缀"u","idi"、"idk"有共同前缀"id"。

从根节点Root开始,每一个节点的孩子节点表示在此节点可以匹配哪些字符,例如根节点有三个孩子节点1,7,11,则根节点处可以匹配u,i,d三个字符,如果目标字符串相应位置上是这3个字符中的一个,则匹配上某一个孩子节点,接下来的匹配将从该孩子节点继续下去。这是可以匹配上的情况,还有不匹配的情况,对于不匹配的情况我们不是跳回到根节点处重新进行匹配(这样会造成目标字符串的回溯),而是模仿KMP算法中失配时,跳转到Failure Node(失配跳转节点)。例如目标字符串是"uuidk",我们按照上面构造出的树,会依次经过1,2,3,4节点(匹配上uuid),4的孩子节点5是i,不能匹配上目标串中的k,这个时候我们应该从4跳转到节点8,我们称8是4的Failure Node。

对于每一个树上的节点都应该有对应的Failure Node,以指示在不匹配的情况应该跳转到哪个节点继续进行匹配。


步骤二:设置每一个节点的Failure Node

上面我们举了一个例子,在节点4失配时,应该跳转到节点8去继续进行匹配,这和KMP算法中在失配时根据失配跳转next数组中记录的位置来进行跳转的原理是一样的,目的是避免对目标串进行回溯匹配。在KMP算法中求next数组值可以用迭代的方法计算出来,但多个模式串的情况下,我们计算Failure Node只能从树的根节点处开始遍历。但是Failure Node的作用和KMP中的next数组值是一样的,查找的原则也是一样的,就是在模式串中查找最长前缀能够匹配上当前失配位置处的最长后缀

仍以上面图中的节点4来举例,到达节点4的模式串为uuid,对应的后缀为"uid","id","d"(在程喆师弟的指正下修改此处笔误),我们要找树的前缀中,能匹配上这三个后缀且长度最长的那个位置,首先看"uid",从树的根节点开始(因为要找前缀)没有能匹配上的,在找"id",找到能匹配上的节点7,8。所以我们设置4的Failure Node为8。如果没有找到能匹配的前缀,则设置Failure Node为根节点Root。

:可能有看过KMP算法的同学会注意到,我们在找到最长后缀的同时还要看后面的孩子节点是否一样,如果找到的位置后面的孩子节点和本节点的孩子节点一样,那么跳转过去,也必然导致失配。实际上确实如此,但我们仍然简单处理,只看前缀和后缀是否匹配,而不管后面的孩子节点是否一样,其原因我们在后面说明。

针对上图的12个节点,每个节点对应的Failure Node如下表:


(图2)

将每个Failure Node不为0的节点,和其对应的Failure Node节点,用绿色虚线连接起来(绿色通道快捷跳转、虚线表示比较隐蔽不容易发现^_^),形成上图。

每个节点K的Failure Node节点的深度不会超过该节点K的深度,因为从跟节点到Failure Node节点是一个前缀。

另外每个节点K的Failure Node节点只有一个,不会有多个。因为Failure Node节点的定义是长度最长的前缀匹配失配位置的后缀,长度最长的只可能找到一处,不可能找到两处,如果有两处长度一样,且都是能够匹配的前缀,那么这两个分支按照前缀树的构造方法应该是重合在一起的。


构造完前缀树,设置好每一个节点的Failure Node之后,我们还有一件重要的事情没有做,观察上面的前缀树,当我们来到节点3时,节点2,3组成的字串"ui"其实已经匹配上一个模式串了,但节点3不是一个模式串的尾字符,所以我们无法报告给查询者,我们其实已经匹配上一个模式串了;另外看节点5,当到达节点5时,我们除了匹配上了"uuidi"字串之外,其实我们也匹配上了"idi","di"字符串。为了解决这个问题,我们需要收集每一个节点的模式串匹配情况。


步骤三:收集每个节点的所有匹配模式串信息

其实要收集每个节点的所有匹配模式串也很简单,观察图2,在节点3位置,应该报告匹配上了模式串"ui",我们可以看到节点3的Failure Node指向的是节点6。所以获取每个节点的所有匹配模式串的信息可以从该节点的Failure Node入口,如果节点K的Failure Node是一个尾节点,那么到达节点K相当于匹配上了一个模式串。另外,我们再观察节点5,节点5本身就是一个尾节点,所以它有自己的匹配模式串,再看5的Failure Node,指向9,节点9也是尾节点,所以5的匹配模式串除了自身的一个模式串(uuidi)之外还包括9所代表的模式串(idi),而9的Failure Node指向12,12也是一个尾节点,所以节点5也也应该包含节点12的匹配模式串(di)……这样进行下去,一直到Failure Node指向了根节点,遍历结束,遍历过程中遇到的所有尾节点都是可匹配的模式串。

在具体的代码实现中,我用一个std::verctor容器来保存一个节点所有的可匹配模式串信息。另外现在我们可以回答一下上面提到的问题。为什么我们没有去检查节点和其Failure Node节点是否有相同的孩子,比如图2中的节点8,我们上面计算出来8的Failure Node是11,但其实因为8有2个孩子9,10,如果8接下来的匹配失配,也就说明目标串中现在出现的字符不是i(9),k(10),而11的孩子节点12表示(i),则通过Failure Node到达11也必然是会匹配失败的。但是我们仍然设置8的Failure Node为11,是因为如果漏掉过了节点11,我们有可能会漏掉匹配的模式串。例如,5的Failure Node是9,9的Failure Node是12,12的Failure Node是7,如果我们因为5,9,12都没有孩子节点,而直接设置节点5的Failure Node为节点7,那么我们在收集所有的匹配模式串信息时,会漏掉尾节点9,12。

可以考虑一个更极端的情况,比如这样的模式串集:"aaaa","aaa","aa","a",如果我们考虑到节点K的Failure Node的孩子结点不应该全都包含在节点K的孩子节点中(和KMP求NEXT数组前缀和后缀的后一个字符不应该一样同理),那么我们在目标串"aaaaaaaaaaa"查找模式串集时会漏掉一些匹配的模式串信息报告。

下图显示了构造树的情况,depth是树的高度,ID=0的节点是Root。Children后面显示的是当前节点的孩子节点,Match pattern后面列出了在该节点位置能匹配上的模式串。

其中:

节点3  Match pattern: "ui"

节点5  Match pattern: "uuidi" "idi" "di"

节点6  Match pattern: "ui"

节点9  Match pattern: "idi" "di"

节点10 Match pattern: "idk"

节点12 Match pattern: "di"


步骤四:对目标串进行搜索匹配

上面3个步骤都完成了之后就可以开始对目标串进行搜索了,很简单的从头到尾线性扫描过程,且目标字符串没有回溯。

搜索之前先记录一个树的当前节点CurNode,初始时,树的当前节点CurNode为根节点Root

从目标串的第一个字符开始,和Root的孩子节点进行匹配,如果不匹配,则目标字符串往后挪一个字符,继续在Root的孩子节点中查找匹配。

如果找到匹配的孩子,则目标字符串往后挪一个字符,CurNode变为匹配上的孩子节点。在接下来的匹配过程中,如果失配将跳转到CurNode节点的Failure Node处继续进行匹配。

在树上每次往孩子节点方向走一步都要检查该孩子节点的匹配模式串信息,如果有匹配的模式串信息,则应该报告搜索者找到了哪些能够匹配的模式串。


对代码的一些说明

代码有两个,1个是在维基百科“Aho–Corasick string matching algorithm”词条给出的链接,地址为:

http://sourceforge.net/projects/multifast/

使用C语言编写,函数式实现,里面含有大量英文注释,理解原理在看代码应该很不难看懂。


另外一个我写的C++代码,使用类的方式,调用起来比较方便,当然我没有加很多的注释,一些细节可能需要阅读者自己琢磨一下。本文中的例子、图片,均来自我实现的代码。

构造树的过程调用

bool CTrie::Create(const MY_PATTERN pattern[], int nCount)
在该函数内部调用了CTrie::SetFailure()设置每个节点 Failure Node,调用CTrie::SetMatchPattern(NODE *pNode)设置每一个节点的所有匹配模式串信息。

因为是多叉树结构,所以上面两个函数都是从根节点向叶子节点的深度优先递归调用。

前缀树构造完毕之后就可以开始在目标字符串中查找匹配的模式串了,例如在目标字符串"hello uuididkidid"中查找图1所示的5个模式串,显示的结果如下:

下标数:0         1         2         3
下标数:0123456789012345678901234567890123456789
目标串:hello uuididkidid
Match pattern at 8:
    "ui"
Match pattern at 10:
    "uuidi"
    "idi"
    "di"
Match pattern at 12:
    "idk"
Match pattern at 15:
    "idi"
    "di"


本文章对应代码下载地址:

http://download.csdn.net/detail/sun2043430/5286986



推荐阅读
  • 云原生边缘计算之KubeEdge简介及功能特点
    本文介绍了云原生边缘计算中的KubeEdge系统,该系统是一个开源系统,用于将容器化应用程序编排功能扩展到Edge的主机。它基于Kubernetes构建,并为网络应用程序提供基础架构支持。同时,KubeEdge具有离线模式、基于Kubernetes的节点、群集、应用程序和设备管理、资源优化等特点。此外,KubeEdge还支持跨平台工作,在私有、公共和混合云中都可以运行。同时,KubeEdge还提供数据管理和数据分析管道引擎的支持。最后,本文还介绍了KubeEdge系统生成证书的方法。 ... [详细]
  • 阿里Treebased Deep Match(TDM) 学习笔记及技术发展回顾
    本文介绍了阿里Treebased Deep Match(TDM)的学习笔记,同时回顾了工业界技术发展的几代演进。从基于统计的启发式规则方法到基于内积模型的向量检索方法,再到引入复杂深度学习模型的下一代匹配技术。文章详细解释了基于统计的启发式规则方法和基于内积模型的向量检索方法的原理和应用,并介绍了TDM的背景和优势。最后,文章提到了向量距离和基于向量聚类的索引结构对于加速匹配效率的作用。本文对于理解TDM的学习过程和了解匹配技术的发展具有重要意义。 ... [详细]
  • 基于PgpoolII的PostgreSQL集群安装与配置教程
    本文介绍了基于PgpoolII的PostgreSQL集群的安装与配置教程。Pgpool-II是一个位于PostgreSQL服务器和PostgreSQL数据库客户端之间的中间件,提供了连接池、复制、负载均衡、缓存、看门狗、限制链接等功能,可以用于搭建高可用的PostgreSQL集群。文章详细介绍了通过yum安装Pgpool-II的步骤,并提供了相关的官方参考地址。 ... [详细]
  • Monkey《大话移动——Android与iOS应用测试指南》的预购信息发布啦!
    Monkey《大话移动——Android与iOS应用测试指南》的预购信息已经发布,可以在京东和当当网进行预购。感谢几位大牛给出的书评,并呼吁大家的支持。明天京东的链接也将发布。 ... [详细]
  • 本文介绍了使用CentOS7.0 U盘刻录工具进行安装的详细步骤,包括使用USBWriter工具刻录ISO文件到USB驱动器、格式化USB磁盘、设置启动顺序等。通过本文的指导,用户可以轻松地使用U盘安装CentOS7.0操作系统。 ... [详细]
  • 本文总结了Java中日期格式化的常用方法,并给出了示例代码。通过使用SimpleDateFormat类和jstl fmt标签库,可以实现日期的格式化和显示。在页面中添加相应的标签库引用后,可以使用不同的日期格式化样式来显示当前年份和月份。该文提供了详细的代码示例和说明。 ... [详细]
  • 推荐一个ASP的内容管理框架(ASP Nuke)的优势和适用场景
    本文推荐了一个ASP的内容管理框架ASP Nuke,并介绍了其主要功能和特点。ASP Nuke支持文章新闻管理、投票、论坛等主要内容,并可以自定义模块。最新版本为0.8,虽然目前仍处于Alpha状态,但作者表示会继续更新完善。文章还分析了使用ASP的原因,包括ASP相对较小、易于部署和较简单等优势,适用于建立门户、网站的组织和小公司等场景。 ... [详细]
  • GetWindowLong函数
    今天在看一个代码里头写了GetWindowLong(hwnd,0),我当时就有点费解,靠,上网搜索函数原型说明,死活找不到第 ... [详细]
  • EPICS Archiver Appliance存储waveform记录的尝试及资源需求分析
    本文介绍了EPICS Archiver Appliance存储waveform记录的尝试过程,并分析了其所需的资源容量。通过解决错误提示和调整内存大小,成功存储了波形数据。然后,讨论了储存环逐束团信号的意义,以及通过记录多圈的束团信号进行参数分析的可能性。波形数据的存储需求巨大,每天需要近250G,一年需要90T。然而,储存环逐束团信号具有重要意义,可以揭示出每个束团的纵向振荡频率和模式。 ... [详细]
  • 本文介绍了使用kotlin实现动画效果的方法,包括上下移动、放大缩小、旋转等功能。通过代码示例演示了如何使用ObjectAnimator和AnimatorSet来实现动画效果,并提供了实现抖动效果的代码。同时还介绍了如何使用translationY和translationX来实现上下和左右移动的效果。最后还提供了一个anim_small.xml文件的代码示例,可以用来实现放大缩小的效果。 ... [详细]
  • Spring源码解密之默认标签的解析方式分析
    本文分析了Spring源码解密中默认标签的解析方式。通过对命名空间的判断,区分默认命名空间和自定义命名空间,并采用不同的解析方式。其中,bean标签的解析最为复杂和重要。 ... [详细]
  • Nginx使用(server参数配置)
    本文介绍了Nginx的使用,重点讲解了server参数配置,包括端口号、主机名、根目录等内容。同时,还介绍了Nginx的反向代理功能。 ... [详细]
  • 搭建Windows Server 2012 R2 IIS8.5+PHP(FastCGI)+MySQL环境的详细步骤
    本文详细介绍了搭建Windows Server 2012 R2 IIS8.5+PHP(FastCGI)+MySQL环境的步骤,包括环境说明、相关软件下载的地址以及所需的插件下载地址。 ... [详细]
  • 这是原文链接:sendingformdata许多情况下,我们使用表单发送数据到服务器。服务器处理数据并返回响应给用户。这看起来很简单,但是 ... [详细]
  • 如何去除Win7快捷方式的箭头
    本文介绍了如何去除Win7快捷方式的箭头的方法,通过生成一个透明的ico图标并将其命名为Empty.ico,将图标复制到windows目录下,并导入注册表,即可去除箭头。这样做可以改善默认快捷方式的外观,提升桌面整洁度。 ... [详细]
author-avatar
soseast9975
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有