双数组Trie树DoubleArrayTrieJava实现.docx
- 文档编号:6356744
- 上传时间:2023-05-09
- 格式:DOCX
- 页数:23
- 大小:135.38KB
双数组Trie树DoubleArrayTrieJava实现.docx
《双数组Trie树DoubleArrayTrieJava实现.docx》由会员分享,可在线阅读,更多相关《双数组Trie树DoubleArrayTrieJava实现.docx(23页珍藏版)》请在冰点文库上搜索。
双数组Trie树DoubleArrayTrieJava实现
双数组Trie树(DoubleArrayTrie)Java实现
双数组Trie树(DoubleArrayTrie)是一种空间复杂度低的Trie树,应用于字符区间大的语言(如中文、日文等)分词领域。
双数组Trie(Double-ArrayTrie)结构由日本人JUN-ICHIAOE于1989年提出的,是Trie结构的压缩形式,仅用两个线性数组来表示Trie树,该结构有效结合了数字搜索树(DigitalSearchTree)检索时间高效的特点和链式表示的Trie空间结构紧凑的特点。
双数组Trie的本质是一个确定有限状态自动机(DFA),每个节点代表自动机的一个状态,根据变量不同,进行状态转移,当到达结束状态或无法转移时,完成一次查询操作。
在双数组所有键中包含的字符之间的联系都是通过简单的数学加法运算表示,不仅提高了检索速度,而且省去了链式结构中使用的大量指针,节省了存储空间。
——《基于双数组Trie树算法的字典改进和实现》
我看了几篇论文,发现中文里也就上面这篇质量最好,英文当属这篇双数组Trie的一种实现。
不过我并不打算按论文的腔调摘抄理论,而是准备借助开源的 darts-java 写点代码分析与笔记,如果能帮到你,实属意外。
darts-java是对 TakuKudo桑的C++版 DoubleArrayTrie的Java移植,代码精简,只有一个Java文件,十分优美。
写一段测试代码
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
Set
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
for (int index :
integerList)
{
System.out.println(words.get(index));
}
}
}
其中small.dic是一个微型的词典:
一举
一举一动
一举成名
一举成名天下知
万能
万能胶
输出:
字典词条:
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.int pos = siblings.get(0).code;
2.while (true)
3.{
4. pos++;
5. begin = pos - siblings.get(0).code; // 当前位置离第一个兄弟节点的距离
6. ……
7.}
3、然后将这群兄弟节点的check设为check[begin+ a1…an]=begin;很显然,叶子节点i的check[i]的值一定等于i,因为它是兄弟节点中的第一个,并且它的code为0。
check[begin + siblings.get(i).code] = begin;
4、接着对每个兄弟节点,如果它没有孩子,也就是上图除root外的绿色节点(叶子节点),令其base为负值;否则为该节点的子节点的插入位置(也就是begin值),同时插入子节点(迭代跳转到步骤2)。
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;
}
这里给出这个例子的basecheck值以及码表,下表中×代表空
码表:
胶 名 动 知 下 成 举 一 能 天 万
33014 21517 21160 30693 19979 25104 20030 19968 33021 22825 19975
DoubleArrayTrie{
size=66039, allocSize=2097152, key=[一举, 一举一动, 一举成名, 一举成名天下知, 万能, 万能胶], keySize=6, progress=6, nextCheckPos=33024, error_=0}
前缀查询
定义当前状态p=base[0]=1。
按照字符串char的顺序walk:
如果base[p]==check[base[p]]&&base[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]开始。
结合例子看可能更清楚:
一举
一举成名
一举成名天下知
稍微解释下
初始空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[20033]==check[20032]说明遇到一个词尾,即查出“一举”
转移p=base[20033] +{char[成]= 25104}+1=20032+ 25104+1= 45137, 检查base[45137]!
=0说明有“成”这个字符。
……
基于双数组Trie树的AhoCorasick自动机
双数组Trie树能高速O(n)完成单串匹配,并且内存消耗可控,然而软肋在于多模式匹配,如果要匹配多个模式串,必须先实现前缀查询,然后频繁截取文本后缀才可多匹配,这样一份文本要回退扫描多遍,性能极低。
AC自动机能高速完成多模式匹配,然而具体实现聪明与否决定最终性能高低。
大部分实现都是一个Map
如果能用双数组Trie树表达AC自动机,就能集合两者的优点,得到一种近乎完美的数据结构。
具体实现请参考《AhoCorasick自动机结合DoubleArrayTrie极速多模式匹配》。
/**
*DoubleArrayTrie:
JavaimplementationofDarts(Double-ARrayTrieSystem)
*/
packagedarts;
importjava.io.BufferedInputStream;
importjava.io.BufferedOutputStream;
importjava.io.DataInputStream;
importjava.io.DataOutputStream;
importjava.io.File;
importjava.io.FileInputStream;
importjava.io.FileOutputStream;
importjava.io.IOException;
importjava.util.ArrayList;
importjava.util.List;
publicclassDoubleArrayTrie{
privatefinalstaticintBUF_SIZE=16384;
privatefinalstaticintUNIT_SIZE=8;//sizeofint+int
privatestaticclassNode{
intcode;
intdepth;
intleft;
intright;
};
privateintcheck[];
privateintbase[];
privatebooleanused[];
privateintsize;
privateintallocSize;
privateList
privateintkeySize;
privateintlength[];
privateintvalue[];
privateintprogress;
privateintnextCheckPos;
//booleanno_delete_;
interror_;
//int(*progressfunc_)(size_t,size_t);
//inline_resizeexpanded
privateintresize(intnewSize){
int[]base2=newint[newSize];
int[]check2=newint[newSize];
booleanused2[]=newboolean[newSize];
if(allocSize>0){
System.arraycopy(base,0,base2,0,allocSize);
System.arraycopy(check,0,check2,0,allocSize);
System.arraycopy(used2,0,used2,0,allocSize);
}
base=base2;
check=check2;
used=used2;
returnallocSize=newSize;
}
privateintfetch(Nodeparent,List
if(error_<0)
return0;
intprev=0;
for(inti=parent.left;i if((length! =null? length[i]: key.get(i).length()) continue; Stringtmp=key.get(i); intcur=0; if((length! =null? length[i]: tmp.length())! =parent.depth) cur=(int)tmp.charAt(parent.depth)+1; if(prev>cur){ error_=-3; return0; } if(cur! =prev||siblings.size()==0){ Nodetmp_node=newNode(); tmp_node.depth=parent.depth+1; tmp_node.code=cur; tmp_node.left=i; if(siblings.size()! =0) siblings.get(siblings.size()-1).right=i; siblings.add(tmp_node); } prev=cur; } if(siblings.size()! =0) siblings.get(siblings.size()-1).right=parent.right; returnsiblings.size(); } privateintinsert(List if(error_<0) return0; intbegin=0; intpos=((siblings.get(0).code+1>nextCheckPos)? siblings.get(0).code+1 : nextCheckPos)-1; intnonzero_num=0; intfirst=0; if(allocSize<=pos) resize(pos+1); outer: while(true){ pos++; if(allocSize<=pos) resize(pos+1); if(check[pos]! =0){ nonzero_num++; continue; }elseif(first==0){ nextCheckPos=pos; first=1; } begin=pos-siblings.get(0).code; if(allocSize<=(begin+siblings.get(siblings.size()-1).code)){ //progresscanbezero doublel=(1.05>1.0*keySize/(progress+1))? 1.05: 1.0 *keySize/(progress+1); resize((int)(allocSize*l)); } if(used[begin]) continue; for(inti=1;i if(check[begin+siblings.get(i).code]! =0) continueouter; break; } //--Simpleheuristics-- //ifthepercentageofnon-emptycontentsincheckbetweenthe //index //'next_check_pos'and'check'isgreaterthansomeconstantvalue //(e.g.0.9), //new'next_check_pos'indexiswrittenby'check'. if(1.0*nonzero_num/(pos-nextCheckPos+1)>=0.95) nextCheckPos=pos; used[begin]=true; size=(size>begin+siblings.get(siblings.size()-1).code+1)? size : begin+siblings.get(siblings.size()-1).code+1; for(inti=0;i check[begin+siblings.get(i).code]=begin; for(inti=0;i List if(fetch(siblings.get(i),new_siblings)==0
- 配套讲稿:
如PPT文件的首页显示word图标,表示该PPT已包含配套word讲稿。双击word图标可打开word文档。
- 特殊限制:
部分文档作品中含有的国旗、国徽等图片,仅作为作品整体效果示例展示,禁止商用。设计者仅对作品中独创性部分享有著作权。
- 关 键 词:
- 双数 Trie DoubleArrayTrieJava 实现