http://www.hankcs.com/program/java/%E5%8F%8C%E6%95%B0%E7%BB%84trie%E6%A0%91doublearraytriejava%E5%AE%9E%E7%8E%B0.html

双数组Trie树(DoubleArrayTrie)是一种空间复杂度低的Trie树,应用于字符区间大的语言(如中文、日文等)分词领域。

双数组Trie (Double-Array Trie)结构由日本人JUN-ICHI AOE于1989年提出的,是Trie结构的压缩形式,仅用两个线性数组来表示Trie树,该结构有效结合了数字搜索树(Digital Search Tree)检索时间高效的特点和链式表示的Trie空间结构紧凑的特点。双数组Trie的本质是一个确定有限状态自动机(DFA),每个节点代表自动机的一个状态,根据变量不同,进行状态转移,当到达结束状态或无法转移时,完成一次查询操作。在双数组所有键中包含的字符之间的联系都是通过简单的数学加法运算表示,不仅提高了检索速度,而且省去了链式结构中使用的大量指针,节省了存储空间。

——《基于双数组Trie树算法的字典改进和实现》

我看了几篇论文,发现中文里也就上面这篇质量最好,英文当属这篇双数组Trie的一种实现。不过我并不打算按论文的腔调摘抄理论,而是准备借助开源的 darts-java 写点代码分析与笔记,如果能帮到你,实属意外。

darts-java 是对 Taku Kudo 桑的 C++ 版 Double Array Trie 的 Java 移植,代码精简,只有一个Java文件,十分优美。

写一段测试代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
package com.hankcs;
 
import darts.DoubleArrayTrie;
 
import java.io.*;
import java.util.*;
 
/**
 * @author hankcs
 */
public class Main
{
    public static void main(String[] args) throws IOException
    {
        BufferedReader reader = new BufferedReader(new FileReader("./data/small.dic"));
        String line;
        List<String> words = new ArrayList<String>();
        Set<Character> charset = new HashSet<Character>();
        while ((line = reader.readLine()) != null)
        {
            words.add(line);
            // 制作一份码表debug
            for (char c : line.toCharArray())
            {
                charset.add(c);
            }
        }
        reader.close();
        // 这个字典如果要加入新词必须按字典序,参考下面的代码
//        Collections.sort(words);
//        BufferedWriter writer = new BufferedWriter(new FileWriter("./data/sorted.dic", false));
//        for (String w : words)
//        {
//            writer.write(w);
//            writer.newLine();
//        }
        System.out.println("字典词条:" + words.size());
 
        {
            String infoCharsetValue = "";
            String infoCharsetCode = "";
            for (Character c : charset)
            {
                infoCharsetValue += c.charValue() + "    ";
                infoCharsetCode += (int)c.charValue() + " ";
            }
            infoCharsetValue += '\n';
            infoCharsetCode += '\n';
            System.out.print(infoCharsetValue);
            System.out.print(infoCharsetCode);
        }
 
        DoubleArrayTrie dat = new DoubleArrayTrie();
        System.out.println("是否错误: " + dat.build(words));
        System.out.println(dat);
        List<Integer> integerList = dat.commonPrefixSearch("一举成名天下知");
        for (int index : integerList)
        {
            System.out.println(words.get(index));
        }
    }
}

其中small.dic是一个微型的词典:

1
2
3
4
5
6
一举
一举一动
一举成名
一举成名天下知
万能
万能胶

输出:

1
2
3
4
5
6
7
字典词条:6
胶    名    动    知    下    成    举    一    能    天    万    
33014 21517 21160 30693 19979 25104 20030 19968 33021 22825 19975 
是否错误: 0
一举
一举成名
一举成名天下知

Trie树的构造与双数组的构造

双数组Trie树归根结底还是属于Trie树,所以免不了有一颗树的构造过程。不过这棵树并没有保存下来,而是边构造树边维护双数组,双数组的信息足以表示整棵树。比如对于上面的例子,首先建立一个空的root节点:

Node{code=0, depth=0, left=0, right=6}

其中code指的是字符的编码,在Java中是双字节,depth是深度,left及right表示这个节点在字典中的索引范围。

比如:

然后按照字典序插入所有的字串节点:

其中绿色节点为空字符,代表从根节点到此节点的路径上的所有节点构成一个词,整个的构建顺序是:

在darts-java中,使用了两个数组base和check来维护Trie树,它们的下标以及值都代表着一个确定的状态。base储存当前的状态以供状态转移使用,check验证字串是否由同一个状态转移而来并且当check为负的时候代表字串结束。(PS 双数组Tire树的具体实现有多种,有的实现将base为负作为状态的结束,大同小异。)

假定有字符串状态s,当前字符串状态为t,假定t加了一个字符c就等于状态tc,加了一个字符x等于状态tx,那么有

base[t] + c = base[tc]

base[t] + x = base[tx]

check[tc] = check[tx]

可见,在单词“一举一动”中,虽然有两个“一”,但它们的前一个状态不同,所以对应的状态分别为“一”和“一举一”,在base数组中的下标不一样。

在每个节点插入的过程中会修改这两个数组,具体说来:

1、初始化root节点base[0] = 1; check[0] = 0;

2、对于每一群兄弟节点,寻找一个begin值使得check[begin + a1...an]  == 0,也就是找到了n个空闲空间,a1…an是siblings中的n个节点对应的code。

1
2
3
4
5
6
7
        int pos = siblings.get(0).code;
        while (true)
        {
            pos++;
            begin = pos - siblings.get(0).code; // 当前位置离第一个兄弟节点的距离
            ……
        }

3、然后将这群兄弟节点的check设为check[begin + a1...an] = begin;很显然,叶子节点i的check[i]的值一定等于i,因为它是兄弟节点中的第一个,并且它的code为0。

1
check[begin + siblings.get(i).code] = begin;

4、接着对每个兄弟节点,如果它没有孩子,也就是上图除root外的绿色节点(叶子节点),令其base为负值;否则为该节点的子节点的插入位置(也就是begin值),同时插入子节点(迭代跳转到步骤2)。

1
2
3
4
5
6
7
8
9
10
            if (fetch(siblings.get(i), new_siblings) == 0)  // 无子节点,也就是叶子节点,代表一个词的终止且不为其他词的前缀
            {
                base[begin + siblings.get(i).code] = -siblings.get(i).left - 1;
                ……
            }
            else
            {
                int h = insert(new_siblings);   // dfs
                base[begin + siblings.get(i).code] = h;
            }

这里给出这个例子的base check值以及码表,下表中×代表空

1
2
3
4
5
6
7
8
9
10
码表:
   胶    名    动    知    下    成    举    一    能    天    万    
33014 21517 21160 30693 19979 25104 20030 19968 33021 22825 19975 
 
DoubleArrayTrie{
char =      ×    一    万     ×    举     ×    动     ×     下    名    ×    知      ×     ×    能    一    天    成    胶
i    =      0 19970 19977 20032 20033 21162 21164 21519 21520 21522 30695 30699 33023 33024 33028 40001 44345 45137 66038
base =      1     2     6    -1 20032    -2 21162    -3     5 21519    -4 30695    -5    -6 33023     3  1540     4 33024
check=      0     1     1 20032     2 21162     3 21519  1540     4 30695     5 33023 33024     6 20032 21519 20032 33023
size=66039, allocSize=2097152, key=[一举, 一举一动, 一举成名, 一举成名天下知, 万能, 万能胶], keySize=6, progress=6, nextCheckPos=33024, error_=0}

前缀查询

定义当前状态p = base[0] = 1。按照字符串char的顺序walk:

如果base[p] == check[p] && base[p] < 0 则查到一个词;

然后状态转移,增加一个字符  p = base[char[i-1]] + char[i] + 1 。加1是为了与null节点区分开。

如果转移后base[char[i-1]] == check[base[char[i-1]] + char[i] + 1],那么下次p就从base[base[char[i-1]] + char[i] + 1]开始。

结合例子看可能更清楚:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
字典词条:6
胶    名    动    知    下    成    举    一    能    天    万    
33014 21517 21160 30693 19979 25104 20030 19968 33021 22825 19975 
是否错误: 0
DoubleArrayTrie{
char =     ×    一    万    ×    举    ×    动    ×    下    名    ×    知    ×    ×    能    一    天    成    胶
i    =      0 19970 19977 20032 20033 21162 21164 21519 21520 21522 30695 30699 33023 33024 33028 40001 44345 45137 66038
base =      1     2     6    -1 20032    -2 21162    -3     5 21519    -4 30695    -5    -6 33023     3  1540     4 33024
check=      0     1     1 20032     2 21162     3 21519  1540     4 30695     5 33023 33024     6 20032 21519 20032 33023
size=66039, allocSize=2097152, key=null, keySize=6, progress=6, nextCheckPos=33024, error_=0}
 
i       =      0     0     1     1     2     2     3     3     4     4     5     5     6     6
b       =      1     1     2     2 20032 20032     4     4 21519 21519  1540  1540     5     5
 
p       =      1 19970     2 20033 20032 45137     4 21522 21519 44345  1540 21520     5 30699
base[p] =      1     2     0 20032    -1     4     0 21519    -3  1540     0     5     0 30695
check[p]=      0     1     0     2 20032 20032     0     4 21519 21519     0  1540     0     5
一举
一举成名
一举成名天下知

稍微解释下

初始空 base[0] = 1, p = 1;

转移 p = base[0] + {char[一] = 19968} + 1 = 1 + 19968 + 1 = 19970,                检查base[19970]!=0说明有“一”这个字符。

而  base[base[19970]] = base[2] = 0 说明没遇到词尾

转移 p = base[19970] + {char[举] = 20030} + 1 = 2 + 20030 + 1 = 20033,            检查base[20033]!=0说明有“举”这个字符。

而  base[base[20033]] = base[20032] = -1 && base[20032] == check[20032] 说明遇到一个词尾,即查出“一举”

转移 p = base[20033] + {char[成] = 25104} + 1 = 20032 + 25104+ 1 = 45137,         检查base[45137]!=0说明有“成”这个字符。

……

转载请注明:码农场 » 双数组Trie树(DoubleArrayTrie)Java实现

双数组Trie树(DoubleArrayTrie)Java实现的更多相关文章

  1. 从Trie树到双数组Trie树

    Trie树 原理 又称单词查找树,Trie树,是一种树形结构,是一种哈希树的变种.它的优点是:利用字符串的公共前缀来减少查询时间,最大限度地减少无谓的字符串比较,能在常数时间O(len)内实现插入和查 ...

  2. python Trie树和双数组TRIE树的实现. 拥有3个功能:插入,删除,给前缀智能找到所有能匹配的单词

    #coding=utf- #字典嵌套牛逼,别人写的,这样每一层非常多的东西,搜索就快了,树高26.所以整体搜索一个不关多大的单词表 #还是O(). ''' Python 字典 setdefault() ...

  3. 双数组Trie树 (Double-array Trie) 及其应用

    双数组Trie树(Double-array Trie, DAT)是由三个日本人提出的一种Trie树的高效实现 [1],兼顾了查询效率与空间存储.Ansj便是用DAT(虽然作者宣称是三数组Trie树,但 ...

  4. [转]双数组TRIE树原理

    原文名称: An Efficient Digital Search Algorithm by Using a Double-Array Structure 作者: JUN-ICHI AOE 译文: 使 ...

  5. Ansj分词双数组Trie树实现与arrays.dic词典格式

    http://www.hankcs.com/nlp/ansj-word-pairs-array-tire-tree-achieved-with-arrays-dic-dictionary-format ...

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

    一 基本构造 Trie树是搜索树的一种,来自英文单词"Retrieval"的简写,可以建立有效的数据检索组织结构,是中文匹配分词算法中词典的一种常见实现.它本质上是一个确定的有限状 ...

  7. 双数组Trie树中叶子结点check[t]=t的证明

    双数组Trie树,其实就是用两个一维数组来表示Trie树这种数据结构. 一个数组称为BASE,另一个数组为CHECK.转移条件如下: 对于状态s,接收字符c,转移到状态t BASE[s]+c=t CH ...

  8. 双数组字典树(Double Array Trie)

    参考文献 1.双数组字典树(DATrie)详解及实现 2.小白详解Trie树 3.论文<基于双数组Trie树算法的字典改进和实现> DAT的基本内容介绍这里就不展开说了,从Trie过来的同 ...

  9. 中文分词系列(一) 双数组Tire树(DART)详解

    1 双数组Tire树简介 双数组Tire树是Tire树的升级版,Tire取自英文Retrieval中的一部分,即检索树,又称作字典树或者键树.下面简单介绍一下Tire树. 1.1 Tire树 Trie ...

随机推荐

  1. 2013-2014 ACM-ICPC, NEERC, Southern Subregional Contest Problem H. Password Service dp

    Problem H. Password Service 题目连接: http://www.codeforces.com/gym/100253 Description Startups are here ...

  2. allowDefinition='MachineToApplication错误

    配置错误说明: 在处理向该请求提供服务所需的配置文件时出错.请检查下面的特定错误详细信息并适当地修改配置文件. 分析器错误信息: 在应用程序级别之外使用注册为 allowDefinition='Mac ...

  3. VIM简单配置(windows)

    set number set history=1000000 set tabstop=4 set shiftwidth=4 set smarttab set nocp filetype plugin ...

  4. SWD Registers

  5. Go 导入当前项目下的包

    其实和其他语言很类似 import (     "../controllers" //这里就是导入上一级目录中的controllers     "./models&quo ...

  6. 如何:声明、实例化和使用委托(C# 编程指南)

    委托的声明如下所示: C#   public delegate void Del<T>(T item); public void Notify(int i) { } C#   Del< ...

  7. JavaScript学习总结(十五)——Function类

    在JavaScript中,函数其实是对象,每个函数都是Function类的实例,既然函数对象,那么就具有自己的属性和方法,因此,函数名实际上也是一个指向函数对象的指针,不会与某个函数绑定. 一.函数的 ...

  8. firedac连接linux的oracle

    firedac连接linux的oracle 首先要下载并安装 oci for linux.OCI的版本要和ORACLE数据库的版本一致. centos7环境下安装OCI:https://blog.cs ...

  9. Unity Shader _Time 的单位

    名称 类型 说明 _Time float4 t 是自该场景加载开始所经过的时间,4个分量分别是 (t/20, t, t*2, t*3) _SinTime float4 t 是时间的正弦值,4个分量分别 ...

  10. HikariCP 脑火Failed to obtain JDBC Connection: You need to run the CLI build and you need target/classes in your classpath to run.

    测试了一下 HikariCP 连接池报错,无解 十一月 16, 2017 5:31:59 下午 org.apache.catalina.core.StandardContext loadOnStart ...