本文介绍了几个常见的匹配算法,通过算法过程和算法分析介绍了各个算法的优缺点和使用场景,并为后续的搜索文章做个铺垫;读者可以通过比较几种算法的差异,进一步了解匹配算法演进过程以及解决问题的场景;KMP算法和Double-Array TireTree是其中算法思想的集大成者,希望读者重点关注。
上文探究了数据结构和算法的一些基础和部分线性数据结构和部分简单非线性数据结构,本文我们来一起探究图论,以及一些字符串模式匹配的高级数据结构和算法。【搜索中常见数据结构与算法探究(一)】(https://developer.jdcloud.com/article/2153)
搜索作为企业级系统的重要组成部分,越来越发挥着重要的作用,ES已经成为每个互联网企业必备的工具集。而作为搜索的基础部分,文本匹配的重要性不言而喻。文本匹配不仅为精确搜索提供了方法,而且为模糊匹配提供了算法依据。比如相似度算法,最大搜索长度算法都是在匹配算法的基础上进行了变种和改良。

以我们物流的抽象模型为例:每个配送中心是一个顶点,由两个顶点表示的配送中心间如果存在一条干线运输线,那么这两个顶点就用一条边连接。边可以由一个权,表示时间、距离和运输的成本。我们愿意迅速确定任何两个配送中心的最佳线路。这里的“最佳”可以是指最少边数的路径,也即经过的配送中心最少;也可以是对一种或所有权总量度所算出的最佳者。
我们考虑实用情况,以有向图为例:
我们假设可以以省会城市开始对顶点编号。如下图

1)邻接矩阵
表示图的一种简单的方法是使用一个二维数据,称为邻接矩阵表示法。有一个二维数组A,对于每条边(u,v),置A[u][v]等于true;否则数组元素就是false。如果边有一个权,那么可以置A[u][v]等于该权,而使用很大或者很小的权作为标记表示不存在的边。虽然这种表示方法的优点是简单,但是,它的空间复杂度为θ(|V|^2),如果图的边不是很多(稀疏的),那么这种表示的代价就太大了。代码如下:
/**
* <p/>
* Description: 使用邻接矩阵的图表示法
* <p/>
* Company: <a href=www.jd.com>京东</a>
*
* @author <a href=mailto:pankun8@jd.com>pankun8</a>
* @date 2021/11/11 15:41
*/
@Data
@NoArgsConstructor
public class Graph<T extends Node>{
/**
* 图的节点数
*/
private int n;
/**
* 图
*/
private T[] data;
/**
* 是否是有向图
*/
private Boolean directed;
/**
* 邻接矩阵
*/
private int[][] matrix;
public Graph(T[] data , Boolean directed){
this.n = data.length;
this.data = data;
this.directed = directed;
matrix = new int[n][n];
}
public void init(T[] data , Boolean directed){
this.n = data.length;
this.data = data;
this.directed = directed;
matrix = new int[n][n];
}
/**
*
* @param v 起点
* @param w 终点
* @param value 权重
*/
public void addEdge(int v , int w , int value){
if((v >=0 && v < n) && (w >= 0 && w < n)){
if(hasEdge(v,w) == value){
return;
}
matrix[v][w] = value;
if(!this.directed){
matrix[w][v] = value;
}
n ++;
}
}
//判断两个节点中是否以及存在边
public int hasEdge(int v, int w){
if((v >=0 && v < n) && (w >= 0 && w < n)){
return matrix[v][w];
}
return 0;
}
/**
* 状态转移函数
* @param index
* @param value
* @return
*/
public int stateTransfer(int index , int value){
int[] matrix = this.matrix[index];
for (int i = 0; i < matrix.length; i++) {
if(matrix[i] == value){
return i;
}
}
return Integer.MAX_VALUE;
}
}
2)邻接表
如果图是稀疏的,那么更好的解决办法是使用邻接表。
从图的某个订单出发,访问途中的所有顶点,并且一个顶点只能被访问一次。图的搜索(遍历)算法常见的有两种,如下:
3.1.1 算法介绍
BF(Brute Force)算法也可以叫暴力匹配算法或者朴素匹配算法。
3.1.2 算法过程
在讲解算法之前,我们先定义两个概念,方便后面讲解。他们分别是主串(S)和模式串(P)。比如说要在字符串A中查找字符串B,那么A就是主串,B就是模式串。我们把主串的长度记作n,模式串的长度记作m,并且n>m。算法过程如下图:

3.1.3 算法分析
BF算法从很“暴力”,当然也就比较简单,好懂,但是响应的性能也不高极端情况下时间复杂度函数为O(m*n)。
尽管理论上BF算法的时间复杂度很高,但在实际的开发中,它却是一个比较常用的字符串匹配算法,主要原因有以下两点:
3.2.1 算法介绍
RK算法全程叫Rabin-Karp算法,是有它的两位发明者Rabin和Karp的名字来命名,这个算法理解并不难,他其实是BF算法的升级版。
3.2.2 算法过程

3.2.3 算法分析
在BF算法中当字符串不匹配时,需要比对每一个字符,如果不能匹配则重新调整I,J的值重新比对每一个字符,RK的思路是将模式串进行哈希算法得到s=hash(P),然后将主串分割成n-m+1个子串,分别对其进行hash算法,然后逐个和s进行比对,减少逐个字符串比对的次数。其中hash函数的具体实现可自行选择。
整个RK算法包含两部分:
第一部分的只需要扫描一遍主串就能计算出所有子串的哈希值,这部分的时间复杂度是O(n)。模式串哈希值与每个子串哈希之间的比较的时间复杂度是O(1),总共需要比对n-m+1次,所以这部分的时间复杂度为O(n)。所以RK算法的整体时间复杂度为O(n)。
3.3.1 算法介绍
KMP算法是一种线性时间复杂度的字符串匹配算法,它是对BF(Brute-Force)算法的改进。KMP算法是由D.E.Knuth与V.R.Partt和J.H.Morris一起发现的,因此人们称它为Knuth-Morris-Pratt算法,简称KMP算法。
前面介绍了BF算法,缺点就是时间消耗很大,KMP算法的主要思想就是:在匹配过程中发生匹配失败时,并不是简单的将模式串P的下标J重新置为0,而是根据一些匹配过程中得到的信息跳过不必要的匹配,从而达到一个较高的匹配效率。
3.3.2 算法过程
在介绍KMP算法之前,首先介绍几个字符串的概念:
例如字符串abcabc
前缀集合是a,ab,abc,abca,abcab
后缀集合为bcabc,cabc,abc,bc,c
最大公共前后缀为abc
KMP算法的过程如下图:

那么为什么KMP算法会知道在匹配失败时下标J回溯的那个位置呢?其实KMP算法在匹配的过程中将维护一些信息来帮助跳过不必要的匹配,这个信息就是KMP算法的重点,next数组也叫做fail数据或者前缀数据。下面我们来分析next数组的由来
对于模式串P的每个元素P[j],都存在一个实数k,使得模式串P开头的k个字符(P[0]P[1]…P[k-1])依次于P[j]前面的k(P[j-k]P[j-k+1]…P[j-1])个字符相同。如果这样的k有多个,则取最大的一个。模式串P中的每个位置j的字符都存在这样的信息,采用next数组表示,即next[j]=MAX{k}。
从上述定义中可看到next(j)的逻辑意义就是求P[0]P[1]…P[j-1]的最大公共前后缀长度。代码如下:
public static void genNext(Integer[] next , String p){
int j = 0 , k = -1;
char[] chars = p.toCharArray();
next[0] = -1;
while(j < p.length() - 1){
if(k == -1 || chars[j] == chars[k]){
j++;k++;
next[j] = k;
}else{
k = next[k];//此处为理解难点
}
}
}
下面分析next的求解过程:
1)特殊情况
当j的值为0或者1的时候,它们的k值都为0,即next(0) = 0 、next(1) = 0。为了后面k值计算的方便,我们将next(0)的值设置为-1。
2)当P[j]==P[k]的情况
当P[j]P[k]时,必然有P[0]…P[k-1]P[j-k]…P[j-1],因此有P[0]…P[k]==P[j-k]…P[j],这样就有next(j+1)=k+1。
3)当P[j]!=P[k]的情况
当P[j]!=P[k]时,必然后next(j)=k,并且next(j+1)<k;也就是说P[0]…P[k-1]=P[j-k]…P[j-1],因此此时k值需要向左移动重新进行匹配,next数组的作用就是在匹配失败时进行下标左移,所以k=next(k)进行下一轮循环。
4)算法优化
上述算法有一个小问题就是当P[k]匹配失败后会跳转到next(k)继续进行匹配,但是此时有可能P[k]=P[next(k)],此时匹配肯定是失败的所以对上述代码进行改进如下:
public void genNext(Integer[] next , String p){
int j = 0 , k = -1;
char[] chars = p.toCharArray();
next[0] = -1;
while(j < p.length() - 1){
if(k == -1 || chars[j] == chars[k]){
j++;k++;
if(chars[j] == chars[k]){
next[j] = next[k];//如果两个相等
}else{
next[j] = k;
}
}else{
k = next[k];
}
}
}
3.3.3 算法分析
KMP算法通过消除主串指针的回溯提高匹配的效率,整个算法分为两部分,next数据的求解,以及字符串匹配,从上一节的分析可知求解next数组的时间复杂度为O(m),匹配算法的时间复杂度为O(n),整体的时间复杂度为O(m+n)。KMP算法不是最快匹配算法,却是名气最大的,使用的范围也非常广。
3.4.1 算法介绍
Boyer-Moore字符串搜索算法是一种非常高效的字符串搜索算法。它由Bob Boyer和J Strother Moore发明,有实验统计它的性能是KMP算法的3-4倍。
3.4.2 算法过程
前面介绍的BF,KMP的算法的匹配过程虽然模式串的回溯过程不同,但是相同点都是从左往右逐个字符进行匹配,而BM算法则是采用的从右向左进行匹配,借助坏字符规则(SKip(j))和好后缀(Shift(j))规则,能够进行快速匹配。其中坏字符和好后缀示意如下图

1)坏字符规则:在BM算法从右向左扫描的过程中,若发现某个字符S[i]不匹配时,则按照如下两种情况进行处理:
2)好后缀规则:在BM算法中,若发现某个字符不匹配的同时,已有部分字符匹配成功,则按照如下两种情况进行处理:
BM算法过程如下:

3.4.3 算法分析
在BM算法中,如果匹配失败则取SKip(j)与Shift(j)中的较大者作为跳跃的距离。BM算法预处理阶段的复杂度为O(m+n),搜索阶段的最好的时间复杂度为O(n/m),最坏的时间复杂为为O(n*m)。由于BM算法采用的是后缀匹配算法,并且通过坏字符和好后缀共同作用下,可以跳过不必要的一些字符,具体Shift(j)的求解过程可参看KMP算法的next()函数过程。
3.5.1 算法介绍
在《搜索中常见的数据结构与算法探究(一)》中,我们介绍过一种树状的数据结构叫做HashTree,本章介绍的TireTree就是HashTree的一个变种。TireTree又叫做字典树或者前缀树,典型的应用是用于统计和排序大量的字符串,所以经常被搜索系统用于文本的统计或搜索。
TireTree的核心思想是空间换时间。TrieTree是一种高效的索引方法,它实际上是一种确定有限自动机(DFA),利用字符串的公共前缀来降低查询时间的开销以达到提高查询效率的目的,非常适合多模式匹配。TireTree有以下基本性质:
3.5.2 算法过程
TireTree构建与查询
我们以《搜索中常见的数据结构与算法探究(一)》案例二中提到的字谜单词为例,共包含this、two、fat和that四个单词,我们来探究一下TireTree的构建过程如下图:

上述过程描述了that,two,fat,that四个单词的插入TireTree的过程,其中黄色的节点代表有单词存在。由于TireTree的构建的过程是树的遍历,所以查询过程和创建过程可以视为一个过程。
TireTree由于本身的特性非常适合前缀查找个普通查找,并且查询的时间复杂度为O(log(n)),和hash比较在一些场景下性能要优于甚至取代hash,例如说前缀查询(hash不支持前缀查询)。
虽然TireTree的查询速度会有一定的提升但是缺不支持后缀查询,并且TireTree对空间利用率不高,且对中文的支持有限。
3.6.1 算法介绍
AC自动机(Aho-Corasick automation)该算法在1975年产生于贝尔实验室,是著名的多模匹配算法之一。要搞懂AC自动机,先得有TireTree和KMP模式匹配算法的基础知识,上述章节有TireTree和KMP算法的详细介绍。
3.6.2 算法过程
AC自动机的构建过程需要如下步骤:
3.6.3 算法分析
AC自动机利用fail指针阻止了模式串匹配阶段的回溯,将时间复杂度优化到了O(n)。
3.7.1 算法介绍
前面提到过TireTree虽然很完美,但是空间利用率很低,虽然可以通过动态分配数组来解决这个问题。为了解决这个问题我们引入Double-Array-TireTree,顾名思义Double-Array-TireTree就是TireTree压缩到两个一维数组BASE和CHECK来表示整个树。Double-Array-TireTree拥有TireTree的所有优点,而且刻服了TireTree浪费空间的不足,使其应用范围更加广泛,例如词法分析器,图书搜索,拼写检查,常用单词过滤器,自然语言处理 中的字典构建等等。
3.7.2 算法过程
在介绍算法之前,我们提前简单介绍一个概念DFA(下一篇详细介绍)。DFA(Deterministic Finite State)有限自动机,通俗来讲DFA是指给定一个状态和一个输入变量,它能转到的下一个状态也就确定下来,同时状态是有限的。
Double-Array-TireTree构建
Double-Array-TireTree终究是一个树结构,树结构的两个重要的要素便是前驱和后继,把树压缩在双数组中,只需要保持能查到每个节点的前驱和后继。首先要介绍几个重要的概念:
从上面的概念的可以理解如下规则,假设一个输入的字符为c,状态从s转移到t
构建的过程大概也分为两种:
我们以静态构建过为核心,我们以《搜索中常见的数据结构与算法探究(一)》案例二中提到的字谜单词为例,共包含this、two、fat和that四个单词为例,其中涉及都的字符集{a,f,h,i,o,s,t,w}共8个字符,为了后续描述方便,我们对这个八个字符进行编码,分别是a-1,f-2,h-3,i-4,o-5,s-6,t-7,w-8
构建this,如下图

构建two,如下图

构建fat,如下图

构建that,如下图

Double-Array-TireTree查询
验证this是否在范围内如下过程
1)state[t] = base[state[null]]+code[t]= 0 + 7=7
check[7]=state[null]=0通过
2)state[th] = base[state[t]]+code[h]=base[7]+3 =2+3=5
check[5]= state[t] = 7通过
3)state[tha] = base[state[th]]+ code[a]=base[5]+1=5+1=6
check[6]=state[th]=5通过
4)state[that] = base[state[tha]]+t = base[6]+7=11
check[11]=state[tha]=6通过
3.7.3 算法分析
通过两个数据base和check将TireTree的数据压缩到两个数组中,既保留了TireTree的搜索的高效,又充分利用了存储空间。
鉴于篇幅有限,DFA,FSA以及FST将在下一篇文章中再来一起讨论,敬请期待!
参考书籍
《数据结构与算法分析:java语言描述》
《自动机理论、语言和计算导论》
本篇文章对本系列的上一篇文章的常见数据结构做了补充,介绍了非线性数据结构的最后一种,图数据结构作为基本数据结构最复杂的一种,在多种企业级应用中都有使用,如网络拓扑,流程引擎,流程编排;另外本文重点介绍了几种常见的匹配算法,以及算法的演进过程和使用场景,为下一篇的主题,也是本系列的重点探究的目标,“搜索”做一个铺垫,敬请期待!
作者: 潘坤 郑冰 曹东杰
我想将html转换为纯文本。不过,我不想只删除标签,我想智能地保留尽可能多的格式。为插入换行符标签,检测段落并格式化它们等。输入非常简单,通常是格式良好的html(不是整个文档,只是一堆内容,通常没有anchor或图像)。我可以将几个正则表达式放在一起,让我达到80%,但我认为可能有一些现有的解决方案更智能。 最佳答案 首先,不要尝试为此使用正则表达式。很有可能你会想出一个脆弱/脆弱的解决方案,它会随着HTML的变化而崩溃,或者很难管理和维护。您可以使用Nokogiri快速解析HTML并提取文本:require'nokogiri'h
我主要使用Ruby来执行此操作,但到目前为止我的攻击计划如下:使用gemsrdf、rdf-rdfa和rdf-microdata或mida来解析给定任何URI的数据。我认为最好映射到像schema.org这样的统一模式,例如使用这个yaml文件,它试图描述数据词汇表和opengraph到schema.org之间的转换:#SchemaXtoschema.orgconversion#data-vocabularyDV:name:namestreet-address:streetAddressregion:addressRegionlocality:addressLocalityphoto:i
有时我需要处理键/值数据。我不喜欢使用数组,因为它们在大小上没有限制(很容易不小心添加超过2个项目,而且您最终需要稍后验证大小)。此外,0和1的索引变成了魔数(MagicNumber),并且在传达含义方面做得很差(“当我说0时,我的意思是head...”)。散列也不合适,因为可能会不小心添加额外的条目。我写了下面的类来解决这个问题:classPairattr_accessor:head,:taildefinitialize(h,t)@head,@tail=h,tendend它工作得很好并且解决了问题,但我很想知道:Ruby标准库是否已经带有这样一个类? 最佳
我使用Nokogiri(Rubygem)css搜索寻找某些在我的html里面。看起来Nokogiri的css搜索不喜欢正则表达式。我想切换到Nokogiri的xpath搜索,因为这似乎支持搜索字符串中的正则表达式。如何在xpath搜索中实现下面提到的(伪)css搜索?require'rubygems'require'nokogiri'value=Nokogiri::HTML.parse(ABBlaCD3"HTML_END#my_blockisgivenmy_bl="1"#my_eqcorrespondstothisregexmy_eq="\/[0-9]+\/"#FIXMEThefoll
给定一个复杂的对象层次结构,幸运的是它不包含循环引用,我如何实现支持各种格式的序列化?我不是来讨论实际实现的。相反,我正在寻找可能会派上用场的设计模式提示。更准确地说:我正在使用Ruby,我想解析XML和JSON数据以构建复杂的对象层次结构。此外,应该可以将该层次结构序列化为JSON、XML和可能的HTML。我可以为此使用Builder模式吗?在任何提到的情况下,我都有某种结构化数据-无论是在内存中还是文本中-我想用它来构建其他东西。我认为将序列化逻辑与实际业务逻辑分开会很好,这样我以后就可以轻松支持多种XML格式。 最佳答案 我最
我正在尝试使用Curbgem执行以下POST以解析云curl-XPOST\-H"X-Parse-Application-Id:PARSE_APP_ID"\-H"X-Parse-REST-API-Key:PARSE_API_KEY"\-H"Content-Type:image/jpeg"\--data-binary'@myPicture.jpg'\https://api.parse.com/1/files/pic.jpg用这个:curl=Curl::Easy.new("https://api.parse.com/1/files/lion.jpg")curl.multipart_form_
无论您是想搭建桌面端、WEB端或者移动端APP应用,HOOPSPlatform组件都可以为您提供弹性的3D集成架构,同时,由工业领域3D技术专家组成的HOOPS技术团队也能为您提供技术支持服务。如果您的客户期望有一种在多个平台(桌面/WEB/APP,而且某些客户端是“瘦”客户端)快速、方便地将数据接入到3D应用系统的解决方案,并且当访问数据时,在各个平台上的性能和用户体验保持一致,HOOPSPlatform将帮助您完成。利用HOOPSPlatform,您可以开发在任何环境下的3D基础应用架构。HOOPSPlatform可以帮您打造3D创新型产品,HOOPSSDK包含的技术有:快速且准确的CAD
目录一.加解密算法数字签名对称加密DES(DataEncryptionStandard)3DES(TripleDES)AES(AdvancedEncryptionStandard)RSA加密法DSA(DigitalSignatureAlgorithm)ECC(EllipticCurvesCryptography)非对称加密签名与加密过程非对称加密的应用对称加密与非对称加密的结合二.数字证书图解一.加解密算法加密简单而言就是通过一种算法将明文信息转换成密文信息,信息的的接收方能够通过密钥对密文信息进行解密获得明文信息的过程。根据加解密的密钥是否相同,算法可以分为对称加密、非对称加密、对称加密和非
本教程将在Unity3D中混合Optitrack与数据手套的数据流,在人体运动的基础上,添加双手手指部分的运动。双手手背的角度仍由Optitrack提供,数据手套提供双手手指的角度。 01 客户端软件分别安装MotiveBody与MotionVenus并校准人体与数据手套。MotiveBodyMotionVenus数据手套使用、校准流程参照:https://gitee.com/foheart_1/foheart-h1-data-summary.git02 数据转发打开MotiveBody软件的Streaming,开始向Unity3D广播数据;MotionVenus中设置->选项选择Unit
文章目录一、概述简介原理模块二、配置Mysql使用版本环境要求1.操作系统2.mysql要求三、配置canal-server离线下载在线下载上传解压修改配置单机配置集群配置分库分表配置1.修改全局配置2.实例配置垂直分库水平分库3.修改group-instance.xml4.启动监听四、配置canal-adapter1修改启动配置2配置映射文件3启动ES数据同步查询所有订阅同步数据同步开关启动4.验证五、配置canal-admin一、概述简介canal是Alibaba旗下的一款开源项目,Java开发。基于数据库增量日志解析,提供增量数据订阅&消费。Git地址:https://github.co