热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

双数组TRIE树原理

原文名称:AnEfficientDigitalSearchAlgorithmbyUsingaDouble-ArrayStructure作者:JUN-IC

 



原文名称:

An Efficient Digital Search Algorithm by Using a Double-Array Structure

作者:

JUN-ICHI AOE

译文:

使用双数组结构的一个高效的Digital Search算法



摘要:

本文介绍了一种新的内部(内部排序的内部,也就是在内存里)数组结构的digital search算法,叫做双数组,结合了数组存取的快速和链式存储的压缩。Digital search树的每一条弧在双数组中都可以以O(1)的时间复杂度计算得到;也就是说,查找一个key值最坏的时间复杂度是O(k),k是这个key值的长度。本文给出了同时具有速度和空间双重性能的双数组的查找,插入,删除算法。假设双数组的长度是n+cm,n是ds树中节点的数量,m是输入符号的数量,c是一个依赖于实现的常数;那么理论上可以证明插入和删除的最坏时间复杂度分别是cm2(插入要解决冲突,所以慢)和cm,与n没有关系。从实验的结果来看,建立双数组的时间随n增长,并且c是一个相当小的常数,从0.17到1.13。



关键词:数据库系统,数据结构,词典,digital search,动态内部存储,关键词查询算法。 ---





1.       导论

在很多信息检索算法中,很需要采用一种快速的digital search算法,或者叫做trie搜索,因为它一个字符(digital)一 个字符地查看输入。使用这种数据结构的例子有一种词法分析器,和一种编译器的本地代码优化器,一种图书搜索,拼写检查,常用单词过滤器,一种自然语言处理 中的形态学分析器等等。词典能够动态增长在自然语言处理中尤为重要,因为经常需要对词汇表添加新词(这其实是双数组的弱项- -)。本文展示的这一算法适合插入远远多于删除的情况,这样删除带来的空间浪费就可以由插入来填补。



       关键词查询策略可以被大致分为两类,按照关键词集合是否可变可以将这些算法分为“动态方法”(允许查询表被修改)和“静态方法”(显然相反)两种。广为人知的“动态方法”有:hashing,二叉树,B+树,扩展hashing,和trie hashing。而“静态方法”有:完美hashing,稀疏表,以及压缩trie。 当使用静态方法的时候我们能专注于提高查询速度和压缩数据结构,而当使用动态方法的时候我们会使用额外的空间以达到更快的更新速度。本文提出的查询方法正 好介于这两者之间,所以我称之为“弱静态方法”。将静态方法扩展到弱静态方法,同时保持前者有用的特性是十分困难的。完美hashing的扩展已经有了,但不能确定插入的时间复杂度上限。本文的目标是建立一种digital search算法,它同时具有静态方法的速度和压缩特性,以及动态方法的快速更新的能力。



       不同于基于key值的搜索方法,digital search采用一连串的字符(digit)来表示一个key。每个h层的DS树的节点表示所有以一定的h个字符开始的关键词;这个节点根据第(h+1)个字符定义它的分支。本文的基本观念是压缩trie树,使用两个一维数组base和check来表示trie树,成为双数组,并且给出更新(插入、删除)算法。Trie的每个节点使用指针指向下一个元素,每个索引元素是一个结束标志加上一个指向新节点的指针(或者null)。查询,插入,删除都非常快,但是它会占用很多空间,因为很多trie树节点是空的;也就是说,trie树是稀疏的。所以我们必须尝试映射节点r到check数组,这种映射关系由base[r]指定。



       在接下来的章节,我们会详细描述我们的想法。在第二节,我们把DS树形式化为模式匹配机器并定义双数组使用O(1)时间计算一条弧。为了将双数组应用于大的关键词集合,双数组做出了一些修改。最主要的创新是仅将足以分辨不通关键词的前缀存储到双数组,将其他部分存储在单独的string里面。插入删除算法在第三章讨论。当插入一个新的非空位置r的时候遇到另一个节点k已经占用这个位置,插入算法通过重新调整base[r]或者base[k]来解决冲突。在本文中,为减少占用的时间和空间占用组少非空未知的k或r有优先权(就是移动占用少的,占用多的不变)。第四节讨论了每个算法的最坏时间复杂度的理论值,并且通过实验验证了。双数组的部分匹配和key-order查询也做出了讨论。最后第五节做出了结论总结。



双数组trie树的基本构造及简单优化



一、 基本构造



Trie树是搜索树的一种,来自英文单词"Retrieval"的简写,可以建立有效的数据检索组织结构,是中文匹配分词算法中词典的一种常见实现。它本质上是一个确定的有限状态自动机(DFA),每个节点代表自动机的一个状态。在词典中这此状态包括"词前缀","已成词"等。

双数组Trie(Double-Array Trie)是trie树的一个简单而有效的实现,由两个整数数组构成,一个是base[],另一个是check[]。设数组下标为i ,如果base,check均为0,表示该位置为空。如果base为负值,表示该状态为词语。Check表示该状态的前一状态,t=base+a, check[t]=i 。



下面举例(源自<<双数组Trie&#xff08;Double-Array Trie&#xff09;的数据结构与具体实现>>)来说明用双数组Trie&#xff08;Double-Array Trie&#xff09;构造分词算法词典的过程。假定词表中只有“啊&#xff0c;阿根廷&#xff0c;阿胶&#xff0c;阿拉伯&#xff0c;阿拉伯人&#xff0c;埃及”这几个词&#xff0c;用Trie树可以表示为&#xff1a;

我们首先对词表中所有出现的10个汉字进行编码&#xff1a;啊-1&#xff0c;阿-2&#xff0c;唉-3&#xff0c;根-4&#xff0c;胶-5&#xff0c;拉-6&#xff0c;及-7&#xff0c;廷-8&#xff0c;伯-9&#xff0c;人-10。。对于每一个汉字&#xff0c;需要确定一个base值&#xff0c;使得对于所有以该汉字开头的词&#xff0c;在双数组中都能放下。例如&#xff0c;现在要确定“阿”字的base值&#xff0c;假设以“阿”开头的词的第二个字序列码依次为a1&#xff0c;a2&#xff0c;a3……an&#xff0c;我们必须找到一个值i&#xff0c;使得base[i&#43;a1]&#xff0c;check[i&#43;a1]&#xff0c;base[i&#43;a2]&#xff0c;check[i&#43;a2]……base[i&#43;an]&#xff0c;check[i&#43;an]均为0。一旦找到了这个i&#xff0c;“阿”的base值就确定为i。用这种方法构建双数组Trie&#xff08;Double-Array Trie&#xff09;&#xff0c;经过四次遍历&#xff0c;将所有的词语放入双数组中&#xff0c;然后还要遍历一遍词表&#xff0c;修改base值。因为我们用负的base值表示该位置为词语。如果状态i对应某一个词&#xff0c;而且Base&#61;0&#xff0c;那么令Base&#61;&#xff08;-1&#xff09;*i&#xff0c;如果Base的值不是0&#xff0c;那么令Base&#61;&#xff08;-1&#xff09;*Base。得到双数组如下&#xff1a;



下标 1 2 3 4 5 6 7 8 9 10 11 12 13 14

Base -1 4 4 0 0 0 0 4 -9 4 -11 -12 -4 -14

Check 0 0 0 0 0 0 0 2 2 2 3 8 10 13

词缀 啊 阿 埃 阿根 阿胶 阿拉 埃及 阿根廷 阿拉伯 阿拉伯人

用上述方法生成的双数组&#xff0c;将“啊”&#xff0c;“阿”&#xff0c;“埃”&#xff0c;“阿根”&#xff0c;“阿拉”&#xff0c;“阿胶”&#xff0c;“埃及”&#xff0c;“阿拉伯”&#xff0c;“阿拉伯人”&#xff0c;“阿根廷”均视为状态。每个状态均对应于数组的一个下标。例如设“阿根”的下标为i&#61;8&#xff0c;那么check的内容是“阿”的下标&#xff0c;而base是“阿根廷”的下标的基值。“廷”的序列码为x&#61;8&#xff0c;那么“阿根廷”的下标为base&#43;x&#61;base[8]&#43;8&#61;12。



二、 基本操作与存在问题



1&#xff0c; 查询

trie树的查询过程其实就是一个DFA的状态转移过程&#xff0c;在双数组中实现起来比较简单&#xff1a;只需按照状态标志进行状态转移即可&#xff0e;例如查询“阿根廷”&#xff0c;先根据“阿”的序列码b&#61;2&#xff0c;找到状态“阿”的下标2&#xff0c;再根据“根”的序列码d&#61;4找到“阿根”的下标base&#43;d&#61;8&#xff0c;同时根据check[base&#43;d]&#61;b&#xff0c;表明“阿根”是某个词的一部分&#xff0c;可以继续查询。然后再找到状态“阿根廷”。它的下标为y&#61;12&#xff0c;此时base[y]<0&#xff0c;check[y]&#61;base&#43;d&#61;8&#xff0c;表明“阿根廷”在词表中&#xff0c;查询完毕。



查询过程中我们可以看到&#xff0c;对于一个词语的查询时间是只与它的长度相关的&#xff0c;也就是说它的时间复杂度为&#xff2f;(1)&#xff0e;在汉语中&#xff0c;词语以单字词&#xff0c;双字词居多&#xff0c;超过三字的词语少之又少&#xff0e;因此&#xff0c;用双数组构建的trie树词典查询是理论上中文机械分词中的最快实现。



2&#xff0c; 插入与删除

双数组的缺点在于&#xff1a;构造调整过程中&#xff0c;每个状态都依赖于其他状态&#xff0c;所以当在词典中插入或删除词语的时候&#xff0c;往往需要对双数组结构进行全局调整,灵活性能较差。



将一个词语插入原有的双数组trie树中&#xff0c;相当于对DFA增加一个状态。首先我们应根据查询方法找出该状态本应所处的位置&#xff0c;如果该位置为空&#xff0c;那好办&#xff0c;直接插入即可。如果该位置不为空。那么我们只好按照构造时一样的方法重新扫描得出该状态已存在的最大前缀状态的BASE值&#xff0c;并由此依次得出该状态后继结点的BASE值。在这其中还要注意CHECK值的相应变化。



例如说&#xff0c;如果&#xff02;阿拉根&#xff02;某一天也成为了一个词&#xff0c;我们要在trie树中插入这一状态。按计算它的位置应在&#xff18;&#xff0c;但8是一个已成状态&#xff0e;所以我们得重新确定&#xff02;阿拉&#xff02;这一最大已成前缀状态的BASE值&#xff0e;重新扫描得出BASE[10]&#61;11。这样状态&#xff11;&#xff15;为&#xff02;阿拉根&#xff02;&#xff0c;且BASE[15]为负&#xff08;成词&#xff09;&#xff0c;CHECK[15]&#61;10&#xff1b;状态&#xff12;&#xff10;为&#xff02;阿拉佰&#xff02;&#xff0c;且BASE[20]&#61;-4&#xff0c;CHECK&#61;10。



这样的处理其实是非常耗时间的&#xff0c;因为得依次对每一个可能BASE值进行扫描来进行确定最大已成前缀状态的BASE值。这个确定过程在构造时还是基本可以忍受的&#xff0c;毕竟你就算用上一&#xff0c;两天来构造也没有问题&#xff08;只要你构造完后可以在效运行即可&#xff09;。但在插入比较频繁时&#xff0c;如果每次都需要那么长的运行时间&#xff0c;那确实是无法忍受的。



双数组删除实现比较简单&#xff0c;只需要将删除词语的对应状态设为空即可――即BASE值&#xff0c;CHECK均为设0。但它存在存在一个空间效率的问题&#xff0e;例如&#xff0c;当我们在上面删除&#xff02;埃及&#xff02;这一词语时&#xff0c;状态11被设为空。而状态10则成了一个无用结点――它不成词&#xff0c;而且在插入新词时也不可重用。所以&#xff0c;随着删除的进行&#xff0c;空状态点和无用状态点不断增多&#xff0c;空间的利用率会不断的降低。



三、 简单优化



优化的基本思路是将双数组trie树构建为一种动态检索方法&#xff0c;从而解决插入和删除所存在的问题。



1&#xff0c; 插入优化

在插入需要确定新的BASE值时&#xff0c;我们是只需要遍历空状态的。非空状态的出现意味着某个BASE值尝试的打败&#xff0c;我们可以完全不必理会。所以&#xff0c;我们可以对所有的空状态构建一个序列&#xff0c;在确定BASE值时只需要扫描该序列即可。

对双数组中的空状态的递增结点r1,r2, …, rm&#xff0c;我们可以这样构建这一空序列&#xff1a;

CHECK[ri]&#61;−ri&#43;1 (1 i m−1),

CHECK[rm]&#61;−(DA_SIZE&#43;1)

其中r1&#61; E_HEAD&#xff0c;为第一个空值状态对应的索引点。这样我们在确定BASE值时只需扫描这一序列即可。这样就省去了对非空状态的访问时间。



这种方法在空状态并不太多的情况下可以很大程度的提高插入速度。



2&#xff0c; 删除优化

1) 无用结点

对于删除叶结点时产生的无用结点&#xff0c;可以通过依次判断将它们置为空&#xff0c;使得可在插入新词时得以重用。例如&#xff0c;如果我们删除了上例中的&#xff02;阿根廷&#xff02;&#xff0c;可以看到&#xff02;阿根&#xff02;这一状态没有子状态&#xff0c;因此也可将它置为空。而&#xff02;阿&#xff02;这一状态不能置空&#xff0c;因为它还有两个子状态。



2) 数组长度的压缩

在删除了一个状态后&#xff0c;数组末尾可能出现的连续空状态我们是可以直接删除的。另外我们还可以重新为最大非空索引点的状态重新确定BASE值&#xff0c;因为它有可能已经由于删除的进行而变小。这们我们可能又得以删除一些空值状态。
推荐阅读
  • 本文深入探讨了NoSQL数据库的四大主要类型:键值对存储、文档存储、列式存储和图数据库。NoSQL(Not Only SQL)是指一系列非关系型数据库系统,它们不依赖于固定模式的数据存储方式,能够灵活处理大规模、高并发的数据需求。键值对存储适用于简单的数据结构;文档存储支持复杂的数据对象;列式存储优化了大数据量的读写性能;而图数据库则擅长处理复杂的关系网络。每种类型的NoSQL数据库都有其独特的优势和应用场景,本文将详细分析它们的特点及应用实例。 ... [详细]
  • PHP预处理常量详解:如何定义与使用常量 ... [详细]
  • 本指南从零开始介绍Scala编程语言的基础知识,重点讲解了Scala解释器REPL(读取-求值-打印-循环)的使用方法。REPL是Scala开发中的重要工具,能够帮助初学者快速理解和实践Scala的基本语法和特性。通过详细的示例和练习,读者将能够熟练掌握Scala的基础概念和编程技巧。 ... [详细]
  • 如何使用 `org.opencb.opencga.core.results.VariantQueryResult.getSource()` 方法及其代码示例详解 ... [详细]
  • 在《Cocos2d-x学习笔记:基础概念解析与内存管理机制深入探讨》中,详细介绍了Cocos2d-x的基础概念,并深入分析了其内存管理机制。特别是针对Boost库引入的智能指针管理方法进行了详细的讲解,例如在处理鱼的运动过程中,可以通过编写自定义函数来动态计算角度变化,利用CallFunc回调机制实现高效的游戏逻辑控制。此外,文章还探讨了如何通过智能指针优化资源管理和避免内存泄漏,为开发者提供了实用的编程技巧和最佳实践。 ... [详细]
  • Flowable 流程图路径与节点展示:已执行节点高亮红色标记,增强可视化效果
    在Flowable流程图中,通常仅显示当前节点,而路径则需自行获取。特别是在多次驳回的情况下,节点可能会出现混乱。本文重点探讨了如何准确地展示流程图效果,包括已结束的流程和正在执行的流程。具体实现方法包括生成带有高亮红色标记的图片,以增强可视化效果,确保用户能够清晰地了解每个节点的状态。 ... [详细]
  • Spring框架中枚举参数的正确使用方法与技巧
    本文详细阐述了在Spring Boot框架中正确使用枚举参数的方法与技巧,旨在帮助开发者更高效地掌握和应用枚举类型的数据传递,适合对Spring Boot感兴趣的读者深入学习。 ... [详细]
  • 本文深入探讨了如何选择适合业务需求的MySQL存储引擎,详细解析了不同存储引擎的特点、适用场景及其在数据存储和管理中的优势。通过对比InnoDB、MyISAM等主流引擎,为读者提供了全面的技术指导和专业建议,帮助开发者在实际应用中做出明智的选择。 ... [详细]
  • 使用 ListView 浏览安卓系统中的回收站文件 ... [详细]
  • 本文详细介绍了 Java 中遍历 Map 对象的几种常见方法及其应用场景。首先,通过 `entrySet` 方法结合增强型 for 循环进行遍历是最常用的方式,适用于需要同时访问键和值的场景。此外,还探讨了使用 `keySet` 和 `values` 方法分别遍历键和值的技巧,以及使用迭代器(Iterator)进行更灵活的遍历操作。每种方法都附有示例代码和具体的应用实例,帮助开发者更好地理解和选择合适的遍历策略。 ... [详细]
  • Web开发框架概览:Java与JavaScript技术及框架综述
    Web开发涉及服务器端和客户端的协同工作。在服务器端,Java是一种优秀的编程语言,适用于构建各种功能模块,如通过Servlet实现特定服务。客户端则主要依赖HTML进行内容展示,同时借助JavaScript增强交互性和动态效果。此外,现代Web开发还广泛使用各种框架和库,如Spring Boot、React和Vue.js,以提高开发效率和应用性能。 ... [详细]
  • 在当前的软件开发领域,Lua 作为一种轻量级脚本语言,在 .NET 生态系统中的应用逐渐受到关注。本文探讨了 Lua 在 .NET 环境下的集成方法及其面临的挑战,包括性能优化、互操作性和生态支持等方面。尽管存在一定的技术障碍,但通过不断的学习和实践,开发者能够克服这些困难,拓展 Lua 在 .NET 中的应用场景。 ... [详细]
  • 本文介绍了如何利用ObjectMapper实现JSON与JavaBean之间的高效转换。ObjectMapper是Jackson库的核心组件,能够便捷地将Java对象序列化为JSON格式,并支持从JSON、XML以及文件等多种数据源反序列化为Java对象。此外,还探讨了在实际应用中如何优化转换性能,以提升系统整体效率。 ... [详细]
  • 在 Linux 环境下,多线程编程是实现高效并发处理的重要技术。本文通过具体的实战案例,详细分析了多线程编程的关键技术和常见问题。文章首先介绍了多线程的基本概念和创建方法,然后通过实例代码展示了如何使用 pthreads 库进行线程同步和通信。此外,还探讨了多线程程序中的性能优化技巧和调试方法,为开发者提供了宝贵的实践经验。 ... [详细]
  • 如何利用Java 5 Executor框架高效构建和管理线程池
    Java 5 引入了 Executor 框架,为开发人员提供了一种高效管理和构建线程池的方法。该框架通过将任务提交与任务执行分离,简化了多线程编程的复杂性。利用 Executor 框架,开发人员可以更灵活地控制线程的创建、分配和管理,从而提高服务器端应用的性能和响应能力。此外,该框架还提供了多种线程池实现,如固定线程池、缓存线程池和单线程池,以适应不同的应用场景和需求。 ... [详细]
author-avatar
书友62423539
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有