双数组Trie树(DoubleArrayTrie)Java实现

时间:2022-12-18 18:02:13

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表示这个节点在字典中的索引范围。

比如:

双数组Trie树(DoubleArrayTrie)Java实现

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

双数组Trie树(DoubleArrayTrie)Java实现

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

双数组Trie树(DoubleArrayTrie)Java实现

在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树的实现&period; 拥有3个功能&colon;插入&comma;删除&comma;给前缀智能找到所有能匹配的单词

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

  3. 双数组Trie树 &lpar;Double-array Trie&rpar; 及其应用

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

  4. &lbrack;转&rsqb;双数组TRIE树原理

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

  5. Ansj分词双数组Trie树实现与arrays&period;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&lbrack;t&rsqb;&equals;t的证明

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

  8. 双数组字典树&lpar;Double Array Trie&rpar;

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

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

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

随机推荐

  1. pwd命令

    [pwd]      打印当前的工作目录             pwd==print work director 命令格式: pwd [OPTION]... 命令功能: 打印当前工作目录的全路径 命 ...

  2. &lbrack;php入门&rsqb; 2、基础核心语法大纲

    1 前言 最近在学PHP,上节主要总结了PHP开发环境搭建<[php入门] 1.从安装开发环境环境到(庄B)做个炫酷的登陆应用>.本节主要总结PHP的核心基础语法,基本以粗轮廓写,可以算作 ...

  3. 从jsTree演示代码中提取的在线文件查看

    从jsTree演示代码中提取的在线文件查看 jsTree 请参考:https://www.jstree.com/ 效果如下: 代码下载:http://files.cnblogs.com/files/z ...

  4. js-string字符串对象

    js-string字符串对象 一.String 对象描述 字符串是 JavaScript 的一种基本的数据类型. String 对象的 length 属性声明了该字符串中的字符数. String 类定 ...

  5. git 的简单使用方法

    git 的简单使用方法1. 服务器 安装完成2. ssh 中的账号创建完成3. 创建 ssh 账号,会在 ssh 的安装目录下的home 目录里面,多了用户家目录4. 进入该目录 ,创建一个新的文件夹 ...

  6. 新的小游戏发布啦。Pop Jungle

    丛林爱消除是一款画面清新,效果绚丽的消除类休闲游戏.你只需要选中尽可能多的图块,并消除它们就可以得到高分,并有无限多的关卡等待你去征服.一旦你开始玩儿你将无法停止下来,如果你还是消除星星的粉丝,那你更 ...

  7. mybatis源码学习: 编译的方法

    mybatis3用了一段时间,抽出时间来研究一下.具体用法参考官方文档就行,源码在这里.mybatis相对而言,规模较小,可以从中学习如何编写高质量的java项目. mybatis3使用maven管理 ...

  8. 18 UI美化状态集合的位图selector

    当我们某个控件 想在不同状态下显示不同的背景图的需求 如我们需要按钮在正常状态显示一种图 按下显示另一背景图 或者单选框被选中时是一种显示图片 没选中是另一种背景图 例子 按钮在不同状态显示不同的背景 ...

  9. mssql 创建存储过程简单实例

    CREATE procedure [dbo].[cp_User_Increment] @channelId int, @currentPage int, @pageSize int, @userId ...

  10. python3中文件操作及编码

    #之前一直没明白文件处理中的w和wb的区别到底是什么,#在看过视频后才知道,原来在linux里面是没有区别的,#但是在windows里面就能够看出区别来了#下面来个例子: with open(&quo ...