双数组Trie树 (Double-array Trie) 及其应用
双数组Trie树(Double-array Trie, DAT)是由三个日本人提出的一种Trie树的高效实现 [1],兼顾了查询效率与空间存储。Ansj便是用DAT(虽然作者宣称是三数组Trie树,但本质上还是DAT)构造词典用作初次分词,极大地节省了内存占用。本文将简要地介绍DAT,并实现了基于DAT的前向最大匹配的中文分词算法。
1. Trie树
两种实现
Trie树(也称为字典树、前缀树)是一种常被用于词检索的树结构,其思想非常简单:利用词的共同前缀以达到节省空间的目的;基本的实现有array与linked-list两种。array实现需要为每一个字符开辟一个字母表大小的数组:
上图给出四个单词bachelor, baby, badge, jar的Trie树array实现示例图;对应的Java代码如下:
class TrieNode {
public Character value;
public TrieNode[] next = new TrieNode[65536]; // 65536 = 2^16
}
虽然,array的查询时间复杂度为\(O(1)\);但是,从图中可以看出,存在着大量的空间浪费。当然,有人会想到用HashMap来代替数组,以减少空间浪费:
class TrieNode {
public Character value;
public Map<Character, TrieNode> next = new HashMap<Character, TrieNode>();
}
mmseg4j便是以此来实现Trie树的。但是,HashMap本质上就是一个hash table;存在着一定程度上的空间浪费。由此,容易想到用linked-list实现Trie树:
虽然linked-list避免了空间浪费,却增加了查询时间复杂度,因为公共前缀就意味着多次回溯。
Double-array实现
Double-array结合了array查询效率高、list节省空间的优点,具体是通过两个数组base
、check
来实现。Trie树可以等同于一个自动机,状态为树节点的编号,边为字符;那么goto函数\(g(r,c) = s\)则表示状态r可以按字符c转移到状态s。base数组便是goto函数array实现,check数组为验证转移的有效性;两个数组满足如下转移方程:
base[r] + c = s
check[s] = r
值得指出的是,代入上述式子中的c为该字符的整数编码值。那么,bachelor, baby, badge, jar的DAT如下图所示:
其中,字符的编码表为{'#'=1, 'a'=2, 'b'=3, 'c'=4, etc. }
。为了对Trie做进一步的压缩,用tail数组存储无公共前缀的尾字符串,且满足如下的特点:
tail of string [b1..bh] has no common prefix and the corresponding state is m:
base[m] < 0;
p = -base[m], tail[p] = b1, tail[p+1] = b2, ..., tail[p+h-1] = bh;
那么,用DAT检索词badge的过程如下:
// root -> b
base[1] + 'b' = 4 + 3 = 7
// root -> b -> a
base[7] + 'a' = 1 + 2 = 3
// root -> b -> a -> d
base[3] + 'd' = 1 + 5 = 6
// badge#
base[6] = -12
tail[12..14] = 'ge#'
至于如何构造数组base、check,可参考原论文 [1]及文章 [2].
2. DAT应用
以下代码分析基于ansj-5.1.1 版本。
词典
Ansj的core.dic
给出中文词典的DAT实现:
249952
37 % 65536 -1 3 {q=1}
39 ' 65536 -1 4 {en=1}
46 . 65536 -1 5 {nb=1}
...
21360 印 92338 -1 2 {j=24, n=1, ng=2, nr=0, v=32}
24230 度 89338 -1 2 {k=0, ng=2, q=28, v=7, vg=2}
27827 河 142597 -1 2 {n=29, q=0}
...
116568 印度 71557 21360 2 {ns=51}
99384 印度河 65536 116568 3 {ns=0}
116553 振臂一 94926 129740 1 null
116566 捅娄子 65536 116571 3 {v=0}
65333 U 65536 -1 4 {en=1}
...
词典共有6列,分别为
index name base check status {词性->词频}
其中,index表示字符串的id(若为单字符,则为其unicode编码对应的整数值),name为词,base、check分别为DAT的base数组、check数组,status记录当前词的状态,最后一列表示词性集合,对应于类org.ansj.domain.AnsjItem
中的成员变量termNatures
。那么,根据DAT的转移方程则有
index['印度'] = 116568 = base['印'] + index['度'] = 92338 + 24230
check['印度'] = 21360 = index['印']
index['印度河'] = 99384 = base['印度'] + index['河'] = 71557 + 27827
check['印度河'] = 116568 = index['印度']
此外,status的数值具有如下含义:
- 1对应的词性为null,name不能单独成词,应继续,比如“振臂一”;
- 2表示name既可单独成词,也可与其他字符组成新词,比如词“印度”;
- 3表示词结束,name成词不再继续,比如词“捅娄子”;
- 4表示英文字母(包括全角)+字符
'
,共计105(26*4+1)个字符; - 5表示数字(包括全角)+小数点,共有21(10*2+1)个字符.
分词
正向最大匹配(Forward Maximum Matching, FMM)的分词思路非常简单:正向匹配词典中的词,取最长匹配者。Scala 2.11 实现FMM如下:
import org.ansj.library.DATDictionary
import scala.collection.mutable.ArrayBuffer
// max-matching algorithm for CWS
def maxMatching(sentence: String): Array[String] = {
val segmented = ArrayBuffer.empty[String]
val chars = sentence.toCharArray
var i = 0
while (i < chars.length) {
DATDictionary.status(chars(i)) match {
// not in core.dic or word-end or last char
case t if t == 0 || t == 3 || i == chars.length - 1 =>
i = singleCharWord(chars, i, segmented)
// word-start
case t if t == 1 || t == 2 =>
i = goOnWord(chars, i, segmented)
// English character or number
case _ =>
i = goOnEnNum(chars, i, segmented)
}
}
segmented.toArray
}
// a single character segment
private def singleCharWord(chars: Array[Char], start: Int, arr: ArrayBuffer[String]): Int = {
arr += chars(start).toString
start + 1
}
// word segment which is in core.dic
private def goOnWord(chars: Array[Char], start: Int, arr: ArrayBuffer[String]): Int = {
var nextIndex: Int = chars(start).toInt
for (j <- start + 1 until chars.length) {
val preIndex = nextIndex
nextIndex = DATDictionary.getItem(nextIndex).getBase + chars(j).toInt
if (DATDictionary.getItem(nextIndex).getCheck != preIndex) {
arr += chars.subSequence(start, j).toString
return j
}
}
chars.length
}
// English chars and numbers compose a word
private def goOnEnNum(chars: Array[Char], start: Int, arr: ArrayBuffer[String]): Int = {
for (j <- start + 1 until chars.length) {
val status = DATDictionary.status(chars(j))
if (status != 4 && status != 5) {
arr += chars.subSequence(start, j).toString
return j
}
}
chars.length
}
函数goOnWord
用到了DAT的转移方程。直观感受下FMM的分词效果:
val sentence = "非农一触即发,现货原油扑朔迷离,伦敦金回暖已定"
println(maxMatching(sentence).mkString("/"))
// 非农/一触即发/,/现货/原油/扑朔迷离/,/伦敦/金/回暖/已/定
我实现了一个DAT生成算法,扔在中文分词项目thulac4j。
3. 参考资料
[1] Aoe, J. I., Morimoto, K., & Sato, T. (1992). An efficient implementation of trie structures. Software: Practice and Experience, 22(9), 695-721.
[2] Theppitak Karoonboonyanan, An Implementation of Double-Array Trie.
双数组Trie树 (Double-array Trie) 及其应用的更多相关文章
- 双数组字典树(Double Array Trie)
参考文献 1.双数组字典树(DATrie)详解及实现 2.小白详解Trie树 3.论文<基于双数组Trie树算法的字典改进和实现> DAT的基本内容介绍这里就不展开说了,从Trie过来的同 ...
- 【转】B树、B-树、B+树、B*树、红黑树、 二叉排序树、trie树Double Array 字典查找树简介
B 树 即二叉搜索树: 1.所有非叶子结点至多拥有两个儿子(Left和Right): 2.所有结点存储一个关键字: 3.非叶子结点的左指针指向小于其关键字的子树,右指针指向大于其关键字的子树: 如: ...
- 中文分词系列(二) 基于双数组Tire树的AC自动机
秉着能偷懒就偷懒的精神,关于AC自动机本来不想看的,但是HanLp的源码中用户自定义词典的识别是用的AC自动机实现的.唉-没办法,还是看看吧 AC自动机理论 Aho Corasick自动机,简称AC自 ...
- 中文分词系列(一) 双数组Tire树(DART)详解
1 双数组Tire树简介 双数组Tire树是Tire树的升级版,Tire取自英文Retrieval中的一部分,即检索树,又称作字典树或者键树.下面简单介绍一下Tire树. 1.1 Tire树 Trie ...
- double array trie 插入结点总结
双数组Trie树索引的可操作性研究.pdf 提示:任一状态点的移动,会影响其Trie树中父节点的base值的选择以及兄弟结点位置的变动,而兄弟结点的移动又须变更相应的子节点的check值. 设待插入的 ...
- 【BZOJ-4212】神牛的养成计划 Trie树 + 可持久化Trie树
4212: 神牛的养成计划 Time Limit: 10 Sec Memory Limit: 512 MBSubmit: 136 Solved: 27[Submit][Status][Discus ...
- 【BZOJ4212】神牛的养成计划 Trie树+可持久化Trie树
[BZOJ4212]神牛的养成计划 Description Hzwer成功培育出神牛细胞,可最终培育出的生物体却让他大失所望...... 后来,他从某同校女神 牛处知道,原来他培育的细胞发生了基因突变 ...
- sphinx索引分析——文件格式和字典是double array trie 检索树,索引存储 – 多路归并排序,文档id压缩 – Variable Byte Coding
1 概述 这是基于开源的sphinx全文检索引擎的架构代码分析,本篇主要描述index索引服务的分析.当前分析的版本 sphinx-2.0.4 2 index 功能 3 文件表 4 索引文件结构 4. ...
- Double Array Trie 的Python实现
不多介绍,可自行Google,或者其它关键词: "datrie" 放代码链接: double_array_trie.py 因为也是一段学习代码,参考的文章都记在里面了,主要参考gi ...
- 双数组trie树的基本构造及简单优化
一 基本构造 Trie树是搜索树的一种,来自英文单词"Retrieval"的简写,可以建立有效的数据检索组织结构,是中文匹配分词算法中词典的一种常见实现.它本质上是一个确定的有限状 ...
随机推荐
- C#高级编程四十九天----队列
队列 1.Queue定义 System.Collections.Queue类表示对象的先进先出集合,存储在Queue(队列)中的对象在一端插入,从还有一端移除. 2.长处 (1).能对集合进行顺序处理 ...
- 文件和目录之mkdir和rmdir函数
用mkdir函数创建目录,用rmdir函数删除目录. #include <sys/stat.h> int mkdir( const char *pathname, mode_t mode ...
- CLOSE_WAIT状态的原因与解决方法 --转
转自:http://blog.chinaunix.net/uid-20357359-id-1963662.html 这个问题之前没有怎么留意过,是最近在面试过程中遇到的一个问题,面了两家公司,两家公司 ...
- iOS之应用程序国际化
一.程序国际化 准备工作: 1.首先我们要先创建一个工程,我们给他命名Internationalization-Demo,然后添加语言. 从代码中分离文本: 目前,应用展示的所有文本都是以硬编码字符串 ...
- error: Error: No resource found that matches the given name (at 'layout_above' with value '@id/btnLayout').
今天在练习fragment碎片的时候,进行界面布局的时候出现了这个问题. 后来解决后发现原因很简单:就是因为在布局xml文件中,引用ID和声明ID的顺序必须保证声明在前,引用在后.和布局的顺序无关. ...
- 混合文件系统(ramdisk+jffs)
背景知识: 一.Ramdisk文件系统: 1.掉电丢失 2.读写速度高 3.数据存储到内存 二.jffs文件系统 1.掉电不丢失 2.可存储于NOR NAND,但是适用于NOR 3.数据存储于flas ...
- jquery - ul li click 无响应
搞了很久, 发现对应jquery来说, 动态产生的ul li(其实不只是这个, 还有 table td等), 直接使用 $("#ul_div>li").click(funct ...
- 淘宝开源Web服务器Tengine安装教程
简介Tengine是由淘宝核心系统部基于Nginx开发的Web服务器,它在Nginx的基础上,针对大访问量网站的需求,添加了很多功能和特性.Tengine的性能和稳定性已经在大型的网站如淘宝网,淘宝商 ...
- ubuntu安装mariadb
参考网址:https://downloads.mariadb.org/mariadb/repositories/ 以ubuntu12.04安装mariadb10为例.具体其他的可以参考给出的参考网址H ...
- OC - 2.OC基础知识介绍
一.基础语法 1> OC语言和C语言 C语言是面向过程的语言,OC语言是面向对象的语言 OC语言继承了C语言,并增加了面向对象的思想 以下内容只介绍OC语言与C语言的不同之处 2> 关键字 ...