Trie树;双数组trie树

Trie树

trie树也叫字典树,多用于单词查找快速检索的树型结构。它是采用空间换时间,利用字符串的公共前缀来压缩字符串降低查询时间。

优点

最大限度的降低无谓字符串的比较,查询效率的到了提升。

缺点

trie对内存消耗大,浪费空间。

三个特性

​ 1.从根节点不包含字符,除根节点外每一个节点都只包含一个字符。

​ 2.从根节点到某一个节点,路径上经过的字符连起来为该节点对应的字符串。

​ 3.每个节点的所有子节点包含的字符都不相同。

trie构建例子

给出一组单词,inn, int, at, age, adv, ant, 我们可以得到下面的Trie:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RfuUpYXA-1631341402308)(/Users/wangxianhu/Desktop/note/image/20200513112535787.png)]

构建思想:当需要插入一个新字符串时,需要先将该字符串进行切分,然后按位逐一从根节点插入。插入过程需要先判断当前位置是否已经存在该前缀的串:

如果存在,则向后挪一位,看下一个节点是否存在。直到找到不存在该串的位置进行插入。

如果不存在,则就在该位置插入即可。

代码简单实现

/**
 * 字典树
 */
class Trie {
   private Node root;
   //初始化字典树
   public Trie(){
       root = new Node();
   }
    /**
     * 插入新词
     * @param word
     */
   public void insert_Word(String word){
       //Node node = root
       insert_Word(this.root,word);
   }
   private void insert_Word(Node root,String word){
    //   word = word.toLowerCase();
       char[] ch = word.toCharArray();
       for (int i=0,len=ch.length;i<len;i++){
           //计算当前字符的位置
           int index = ch[i]-'A';
           if (root.getChild()[index]==null){
               root.getChild()[index] = new Node();
           }
           if (i==len-1){
               root.getChild()[index].setLeaf(true);
           }
           root = root.getChild()[index];
       }
   }
   public boolean wordIsExist(String word){
      return search_Word(this.root,word);
   }
   private boolean search_Word(Node root,String word){
      // word = word.toLowerCase();
       char[] chars = word.toCharArray();
       for (int i = 0,len = chars.length; i < len ; i++) {
           int index = chars[i]-'A';
           if (root.getChild()[index]==null){
               return false;
           }
           root = root.getChild()[index];
       }
       return true;
   }
}
/**
 * 节点
 */
class Node{
    private Node[] child;
    //是否到叶子节点(表示当前的串是否结束)
    private boolean isLeaf;

    public Node() {
        this.child = new Node[52];
        this.isLeaf = false;
    }

    public Node[] getChild() {
        return child;
    }

    public void setChild(Node[] child) {
        this.child = child;
    }

    public boolean isLeaf() {
        return isLeaf;
    }

    public void setLeaf(boolean leaf) {
        isLeaf = leaf;
    }
}

双数组Trie树

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

DFA的特征:

有一个有限状态集合和一些从一个状态通向另一个状态的边,每条边上标记有一个符号,其中一个状态是初态,某些状态是终态。但不同于不确定的有限自动机,DFA中不会有从同一状态出发的两条边标志有相同的符号。
对于DFA来说,每个节点代表一个“状态”,每条边代表一个“变量”。

简单点说就是,它是是通过event和当前的state得到下一个state,即event+state=nextstate。理解为系统中有多个节点,通过传递进入的event,来确定走哪个路由至另一个节点,而节点是有限的。

优点

空间复杂度低,节省了内存占用。

缺点

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

定义

将原来需要多个数组才能表示的Trie树,使用两个数组就可以存储下来,可以极大的减小空间复杂度。由于用base和check两个数组构成,又称为双数组字典树。
具体来说就是使用两个数组base[]和check[]来维护Trie树,base[]负责记录状态,check[]用于检验状态转移的正确性,当check[i]为负值时,表示此状态为字符串的结束。
具体来说,当状态b接受字符c然后转移到状态p的时候,满足的状态转移公式如下:

base[s] + c = t   // 表示 当前base[s]的状态+c状态======>t状态
check[t] = s //检查转变后的t状态是否==s状态 
//双数组Trie必须满足上述公式  
//-------------------------  
在状态进行转换过程中可能会出现地址冲突(也就是说两个状态转化后具有相同的地址)
  即前一个状态转换后的地址为9 此时地址为9的地方被占用。但是,下一个状态转换的地址也为9 因为此时地址9已经被使用了 所以新状态转换后不             	 可能继续存储在9号位置(即出现地址冲突)。
//出现地址冲突的解决方式
  利用while循环从发生冲突的位置一直前前遍历直到找到一个地址不冲突的位置。然后进行对c值的改变。
//构造双数组最好先构建每个词的首字然后在构建子节点
  因为这样的构建方式可以避免多次重构建。(否则当插入一个未出现过的词,会导致根节点的变化)
//叶子节点的处理方式
  1.将每个词尾设置为特殊字符('\0',但是这样的构建方式会消耗空间。
  2.将每个词的词尾设置为转移基数的负值(即-c),可以节省构建时间.

Base Array 的作用

双数组 Trie 树和经典 Trie 树一样,也是用数组实现 Trie 树。只不过它是将所有节点的状态都记录到一个数组之中(Base Array),以此避免数组的大量空置。以行文开头的示例为例,每个字符在 Base Array 中的状态可以是这样子的:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-e8FF2Q61-1631341402311)(/Users/wangxianhu/Desktop/note/image/165561480-58d8851fe8744_fix732.png)]

事实上,为了能使单个数组承载更多的信息,Base Array 仅仅会通过数组的位置记录下字符的状态(节点),比如用数组中的位置 2 指代“清”节点、 位置 7 指代 “中”节点;而数组中真正存储的值其实是一个整数,这个整数我们称之为“转移基数”,比如位置2的转移基数为 base[2]=3位置7的转移基数为base[7]=2因此在不考虑叶子节点的情况下, Base Array 是这样子的:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UZQQjO3a-1631341402314)(/Users/wangxianhu/Desktop/note/image/1883829895-58d88d1549aa9_fix732.png)]

转移基数是为了在一维数组中实现 Trie 树中字符的链路关系而设计的,举例而言,如果我们知道一个词中某个字符节点的转移基数,那么就可以据此推断出该词下一个节点在 Base Array 中的位置:比如知道 “清华”首字的转移基数为base[2]=3,那么“华”在数组中的位置就为base[2]+code("华"),这里的code("华")为字符表中“华”的编码,假设例树的字符编码表为:

清-1,华-2,大-3,学-4,新-5,中-6,人-7

那么“华”的位置应该在Base Array 中的的第 5 位(base[2]+code("华")=3+2=5):

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-EX0xC5hl-1631341402315)(/Users/wangxianhu/Desktop/note/image/2949658153-58d8b576d88a7_fix732.png)]

而所有词的首字,则是通过根节点的转移基数推算而来。因此,对于字典中已有的词,只要我们每次从根节点出发,根据词典中各个字符的编码值,结合每个节点的转移基数,通过简单的加法,就可以在Base Array 中实现词的链路关系。以下是“清华”、“清华大学”、“清新”、“中华”、“华人”五个词在 Base Array 中的链路关系:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cCcpYKjr-1631341402317)(/Users/wangxianhu/Desktop/note/image/2019361853-58d8b7821fa95_fix732.png)]

Base Array 的构造

Base Array 不仅能够表达词典中每个字符的状态,而且还能实现高效的状态转移。

事实上,同样一组词和字符编码,以不同的顺序将字符写入 Trie 树中,获得的 Base Array 也是不同的,以“清华”、“清华大学”、“清新”、“中华”、“华人”五个词,以及字符编码:[清-1,华-2,大-3,学-4,新-5,中-6,人-7] 为例,在不考虑叶子节点的情况下,两种处理方式获得的 base array 为:

  1. 首先依次处理“清华”、“清华大学”、“清新”、“中华”、“华人”五个词的首字,然后依次处理所有词的第二个字…直到依次处理完所有词的最后一个字,得到的 Base Array 为:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JHB3WjKi-1631341402320)(/Users/wangxianhu/Desktop/note/image/1883829895-58d88d1549aa9_fix732.png)]

  1. 依次处理“清华”、“清华大学”、“清新”、“中华”、“华人”五个词中的每个字,得到的 Base Array 为:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-S0eA8o0y-1631341402321)(/Users/wangxianhu/Desktop/note/image/540041318-58d8dce299563_fix732.png)]

可以发现,不同的字符处理顺序,得到的 Base Array 存在极大的差别:两者各状态的转移基数不仅完全不同,而且 Base Array 的长度也有差别。然而,两者获得的方法却是一致的,下面以第一种字符处理顺序讲解一下无叶子节点的 Base Array 构建:

  1. 首先人为赋予根节点的转移基数为1(可自定义,详见下文),然后依次将五个词中的首字"清"、“中”、“华”写入数组之中,写入的位置由base[1]+code(字符)确定,每个位置的转移基数(base[i])等于上一个状态的转移基数(此例也即base[1]),这个过程未遇到冲突,最终结果见下图:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-O85TTzSj-1631341402323)(/Users/wangxianhu/Desktop/note/image/110165769-58d8c9d01b494_fix732.png)]

  1. 然后依次处理每个词的第二个字,首先需要处理的是“清华”这个词的“华”字,程序先从根节点出发,通过base[1]+code(“清”)=2找到“清”节点,然后以此计算“华”节点应写入的位置,通过计算base[2]+code(“华”)=3 寻找到位置 3,却发现位置3已有值,于是后挪一位,在位置4写入“华”节点,由于“华”节点未能写入由前驱节点“清”预测的位置,因此为了保证通过“清”能够找到“华”,需要重新计算“清”节点的转移基数,计算公式为4-code(“华”)=2,获得新的转移基数后,改写“清”节点的转移基数为2,然后将“华”节点的转移基数与“清”节点保持一致,最终结果为:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PrpQt72s-1631341402325)(/Users/wangxianhu/Desktop/note/image/1876219904-58d8ca47111b0_fix732.png)]

  1. 重复上面的步骤,最终获得整个 Base Array:

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yNGmxzwX-1631341402326)(/Users/wangxianhu/Desktop/note/image/948515066-58d8ca225bd11_fix732 (1)].png)

叶子节点的处理

上面关于 Base Array 的叙述,只涉及到了根节点、分支节点的处理,事实上,Base Array 同样也需要负责叶子节点的表达,但是由于叶子节点的处理,具体的实现各不一致,一般词的最后一个字都不需要再做状态转移,因此有人建议将词的最后一个节点的转移基数统一改为某个负数(比如统一设置为-2),以表示叶子节点,按照这种处理,对于示例而言,base array 是这样的:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-H6PXsCfN-1631341402328)(/Users/wangxianhu/Desktop/note/image/4025668869-58da627ad9931_fix732.png)]

可能会发现,“清华” 和 “清华大学” 这两个词中,只有“清华大学”有叶子节点,既是公共前缀又是单个词的“清华”实际上无法用这种方法表示出叶子节点。

也有人建议为词典中所有的词增加一个特殊词尾(比如将“清华”这个词改写为“清华\0”),再将这些词构建为树,特殊字符词尾节点的转移基数统一设置设为-2,以此作为每个词的叶子节点[4]。这种方法的好处是不用对现有逻辑做任何改动,坏处是增加了总节点的数量,相应的会增加词典构建的时长和空间的消耗。

最后,给出一个新的处理方式:直接将现有 base array 中词尾节点的转移基数取负,而数组中的其他信息不用改变。

以树例为例,处理叶子节点前,Base Array 是这样子的:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-snKQZT8g-1631341402330)(/Users/wangxianhu/Desktop/note/image/948515066-58d8ca225bd11_fix732 (2)].png)

处理叶子节点之后,Base Array 会是这样子的:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZPw1lfee-1631341402331)(/Users/wangxianhu/Desktop/note/image/1943833000-58da7b4b98edf_fix732.png)]

每个位置的转移基数绝对值与之前是完全相同的,只是叶子节点的转移基数变成了负数,这样做的好处是:不仅标明了所有的叶子节点,而且程序只需对状态转移公式稍加改变,便可对包括“清华”、“清华大学”这种情况在内的所有状态转移做一致的处理,这样做的代价就是需要将状态转移函数base[s]+code(字符)改为|base[s]|+code(字符),意味着每次转移需要多做一次取绝对值运算,不过好在这种处理对性能的影响微乎其微。

Check Array 的构造

“双数组 Trie 树”,必定是两个数组,因此单靠 Base Array 是玩不起来的…上面介绍的 Base Array 虽然解决了节点存储和状态转移两个核心问题,但是单独的 Base Array 仍然有个问题无法解决:

Base Array 仅仅记录了字符的状态,而非字符本身,虽然在 Base Array,字典中已有的任意一个词,其链路都是确定的、唯一的,因此并不存在歧义;但是对于一个新的字符串(不管是检索字符串还是准备为字典新增的词),Base Array 是不能确定该词是否位于词典之中的。对于这点,我们举个例子就知道了:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3u1YsFhT-1631341402333)(/Users/wangxianhu/Desktop/note/image/1943833000-58da7b4b98edf_fix732 (1)].png)

如果我们要在例树中确认外部的一个字符串“清中”是否是一个词,按照 Trie 树的查找规则,首先要查找“清”这个字,我们从根节点出发,获得|base[1]|+code(“清”)=3,然后转移到“清”节点,确认清在数组中存在,我们继续查找“中”,通过|base[3]|+code(“中”)=9获得位置9,字符串此时查询完毕,根据位置9的转移基数base[9]=-2确定该词在此终结,从而认为字符串“清中”是一个词。而这显然是错误的!事实上我们知道 “清中”这个词在 base array 中压根不存在,但是此时的 base array 却不能为此提供更多的信息。

为了解决这些问题,双数组 Trie 树专门设计了一个 check 数组:

check array 与 base array 等长,它的作用是标识出 base array 中每个状态的前一个状态,以检验状态转移的正确性。

因此, 例树的 check array 应为:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9hJL6k1l-1631341402334)(/Users/wangxianhu/Desktop/note/image/1423114938-58da86d26f292_fix732.png)]

如图,check array 元素与 base array 一一对应,每个 check array 元素标明了base array 中相应节点的父节点位置,比如“清”节点对应的check[2]=0,说明“清”节点的父节点在 base array 的0 位(也即根节点)。对于上例,程序在找到位置9之后,会检验 check[9]==2,以检验该节点是否与“清”节点处于同一链路,由于check[9]!=2,那么就可以判定字符串“清中”并不在词典之中。

综上,check array 巧妙的利用了父子节点间双向关系的唯一性(公式化的表达就是base[s]+c=t & check[t]=s是唯一的,其中 s为父节点位置,t为子节点位置),避免了 base array 之中单向的状态转移关系所造成的歧义(公式化的表达就是base[s]+c=t)。

(本文参考https://segmentfault.com/a/1190000008877595 )具体细节也可以参考阅读

一些开源的实现

darts-java

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

在darts-java中,使用了两个数组base和check来维护Trie树,它们的下标以及值都代表着一个确定的状态。base储存当前的状态以供状态转移使用,check验证字串是否由同一个状态转移而来并且当check为负的时候代表字串结束。

//定义部分源码
 private final static int BUF_SIZE = 65536;
 private final static int UNIT_SIZE = 8; // size of int + int

	private static class Node {
		int code; //字符编码
		int depth; //深度
		int left;  //索引范围左限制
		int right; //索引范围右限制
	};
	private int check[];
	private int base[];
	private boolean used[];
	private int size;
	private int allocSize;
	private List<String> key;
	private int keySize;
	private int length[];
	private int value[];
	private int progress;
	private int nextCheckPos;
	// boolean no_delete_;
	int error_;

​ darts-java实现了分词 前缀查询 精确索引

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qXK1DzWX-1631341402336)(/Users/wangxianhu/Desktop/note/image/0E2967FA-7C9B-4140-9BB9-ADFAEB82FCA0.png)]

​ 开源地址:https://github.com/komiya-atsushi/darts-java

Aho Corasick自动机结合DoubleArrayTrie极速多模式匹配

//构建部分源
public class AhoCorasickDoubleArrayTrie<V> implements Serializable
{
    /**
     * check array of the Double Array Trie structure
     */
    protected int[] check;
    /**
     * base array of the Double Array Trie structure
     */
    protected int[] base;
    /**
     * fail table of the Aho Corasick automata
     */
    protected int[] fail;
    /**
     * output table of the Aho Corasick automata
     */
    protected int[][] output;
    /**
     * outer value array
     */
    protected V[] v;

    /**
     * the length of every key
     */
    protected int[] l;

    /**
     * the size of base and check array
     */
    protected int size;
}
// AC自动机的基础(success表)就是Trie树,只不过比Trie树多了output表和fail表。那么AhoCorasickDoubleArrayTrie的构建原理就是为每个状态(base[i]和check[i])构建output[i][]和fail[i]。

​ Double Array Trie实现了一个性能极高的Aho Corasick自动机,应用于分词可以取得1400万字每秒,约合27MB/s的分词速度。其中词典为150万词,构建耗时1801 ms 。AC自动机能高速完成多模式匹配,然而具体实现聪明与否决定最终性能高低。双数组Trie树能高速O(n)完成单串匹配,并且内存消耗可控,然而软肋在于多模式匹配,如果要匹配多个模式串,必须先实现前缀查询,然后频繁截取文本后缀才可多匹配,这样一份文本要回退扫描多遍,性能极低。如果能用双数组Trie树表达AC自动机,就能集合两者的优点,得到一种近乎完美的数据结构。在我的Java实现中,我称其为AhoCorasickDoubleArrayTrie,支持泛型和持久化。 -----来自作者。

​ AhoCorasickDoubleArrayTrie 也支持精确单模式匹配,前缀匹配,多模式匹配等。

​ 开源地址:https://github.com/hankcs/AhoCorasickDoubleArrayTrie

```java

/**

  • 匹配母文本

  • @param text 一些文本

  • @return 一个pair列表
    */
    public List<Hit> parseText(String text)

    
    
/**
 * 一个命中结果
 *
 * @param <V>
 */
public class Hit<V>
{
    /**
     * 模式串在母文本中的起始位置
     */
    public final int begin;
    /**
     * 模式串在母文本中的终止位置
     */
    public final int end;
    /**
     * 模式串对应的值
     */
    public final V value;
}
/**
 * 处理文本
 *
 * @param text      文本
 * @param processor 处理器
 */
public void parseText(String text, IHit<V> processor) //返回一个巨大的List并不是个好主意,AhoCorasickDoubleArrayTrie提供即时处理的结构
  
  
 /**
 * 命中一个模式串的处理方法
 */
public interface IHit<V>
{
    /**
     * 命中一个模式串
     *
     * @param begin 模式串在母文本中的起始位置
     * @param end   模式串在母文本中的终止位置
     * @param value 模式串对应的值
     */
    void hit(int begin, int end, V value);
}

测试

//简单测试时使用的一些数据放入数组 然后构建双数组进行关键字匹配  
String[] keyArray = new String[]
                {
                        "hers",
                        "his",
                        "she",
                        "he",
                        "你",
                        "你好"
                };
        for (String key : keyArray)
        {
            map.put(key, key);
        }
        // Build an AhoCorasickDoubleArrayTrie
        AhoCorasickDoubleArrayTrie<String> acdat = new AhoCorasickDoubleArrayTrie<String>();
        acdat.build(map);

   //关键词查找
    private void validateASimpleAhoCorasickDoubleArrayTrie(AhoCorasickDoubleArrayTrie<String> acdat)
    {
        // Test it
        final String text = "uhers你好";
        acdat.parseText(text, new AhoCorasickDoubleArrayTrie.IHit<String>()
        {
            @Override
            public void hit(int begin, int end, String value)
            {
              //  System.out.printf("[%d:%d]=%s\n", begin, end, value);
                assertEquals(text.substring(begin, end), value);
            }
        });
        // Or simply use
        List<AhoCorasickDoubleArrayTrie.Hit<String>> wordList = acdat.parseText(text);
        System.out.println(wordList);
    }

输出:[[1:3]=he, [1:5]=hers, [5:6]=, [5:7]=你好]
  
  
  //对大量的文本数据的匹配
   public void testBuildAndParseWithBigFile() throws Exception
    {
        Set<String> dictionary = loadDictionary("cn/wordlist.txt");
        final String text = loadText("cn/text.txt");
        Map<String, String> map = new TreeMap<String, String>();
        for (String key : dictionary)
        {
            map.put(key, key);
        }
        // Build an AhoCorasickDoubleArrayTrie
        AhoCorasickDoubleArrayTrie<String> acdat = new AhoCorasickDoubleArrayTrie<String>();
        acdat.build(map);
        acdat.parseText(text, new AhoCorasickDoubleArrayTrie.IHit<String>()
        {
            @Override
            public void hit(int begin, int end, String value)
            {
                assertEquals(text.substring(begin, end), value);
            }
        });
        List<AhoCorasickDoubleArrayTrie.Hit<String>> wordList = acdat.parseText(text);
        System.out.println(wordList);
    }
//文本数据量为17315行。检测其中包含抗战的关键字
//输出结果 [[40:42]=抗战, [42:42]=, [38425:38427]=抗战, [38427:38427]=, [54878:54880]=抗战, [54880:54880]=, [78535:78537]=抗战, [78537:78537]=, [439895:439897]=抗战, [439897:439897]=, [773437:773439]=抗战, [773439:773439]=]

//关于分词效率 作者给出数据 真实应用环境中在132 ms内分完了整本《我的团长我的团》.txt,共774165字,速度是5864886.36 字/秒
//也可以浏览 http://www.hankcs.com/program/algorithm/aho-corasick-double-array-trie.html



//性能 AhoCorasickDoubleArrayTrie with robert-bor's aho-corasick,
Parsing English document which contains 3409283 characters, with a dictionary of 127142 words.
               	aho-corasick    AhoCorasickDoubleArrayTrie
time           	607            	102
char/s         	5616611.20     	33424343.14
rate           	1.00           	5.95
===========================================================================
Parsing Chinese document which contains 1290573 characters, with a dictionary of 146047 words.
               	aho-corasick    AhoCorasickDoubleArrayTrie
time           	319            	35
char/s         	2609156.74     	23780600.00
rate           	1.00           	9.11
===========================================================================
 

HanLP自然语言处理包开源

​ 支持中文分词(N-最短路分词、CRF分词、索引分词、用户自定义词典、词性标注),命名实体识别(中国人名、音译人名、日本人名、地名、实体机构名识别),关键词提取,自动摘要,短语提取,拼音转换,简繁转换,文本推荐,依存句法分析(MaxEnt依存句法分析、神经网络依存句法分析)。提供Lucene插件,兼容Solr和ElasticSearch。

​ HanLP已经被广泛用于Lucene、Solr、ElasticSearch、Hadoop、Android、Resin等平台,有大量开源作者开发各种插件与拓展,并且被包装或移植到Python、C#、R、JavaScript等语言上去。

​ AhoCorasickDoubleArrayTrie 也集成在HanLP中。

中文分词

   最短路分词

   N-最短路分词

   CRF分词

   索引分词

   极速词典分词

   用户自定义词典

词性标注

命名实体识别

   中国人名识别

   音译人名识别

   日本人名识别

   地名识别

   实体机构名识别

关键词提取

   TextRank关键词提取

自动摘要

   TextRank自动摘要

短语提取

   基于互信息和左右信息熵的短语提取

拼音转换

   多音字

   声母

   韵母

   声调

简繁转换

   繁体中文分词

   简繁分歧词

文本推荐

   语义推荐

   拼音推荐

   字词推荐

依存句法分析

   基于神经网络的高性能依存句法分析器

   MaxEnt依存句法分析

   CRF依存句法分析

语料库工具

   分词语料预处理

   词频词性词典制作

   BiGram统计

   词共现统计

   CoNLL语料预处理

   CoNLL UA/LA/DA评测工具

开源地址:https://github.com/hankcs/HanLP/tree/1.x

文档介绍地址:http://www.hankcs.com/nlp/hanlp.html

集成elasticsearch-hanlp(安装见:https://github.com/hualongdata/hanlp-ext/tree/master/es-plugin

一些分词器对比

分词器优势劣势
Smart Chinese Analysis官方插件中文分词效果惨不忍睹
IKAnalyzer简单易用,支持自定义词典和远程词典词库需要自行维护,不支持词性识别
结巴分词新词识别功能不支持词性识别
Ansj中文分词分词精准度不错,支持词性识别对标hanlp词库略少,学习成本高
Hanlp目前词库最完善,支持的特性非常多需要更优的分词效果,学习成本高

分词准确性从高到低依次是:hanlp> ansj >结巴>IK>Smart Chinese Analysis

1、word分词 最大Ngram分值算法:
分词速度:370.9714 字符/毫秒
行数完美率:66.55%  行数错误率:33.44%  总的行数:2533709  完美行数:1686210  错误行数:847499
字数完美率:60.94% 字数错误率:39.05% 总的字数:28374490 完美字数:17293964 错误字数:11080526

2、word分词 最少词数算法:
分词速度:330.1586 字符/毫秒
行数完美率:65.67%  行数错误率:34.32%  总的行数:2533709  完美行数:1663958  错误行数:869751
字数完美率:60.12% 字数错误率:39.87% 总的字数:28374490 完美字数:17059641 错误字数:11314849

3、HanLP分词器 标准分词:
分词速度:935.7724 字符/毫秒
行数完美率:58.31%  行数错误率:41.68%  总的行数:2533709  完美行数:1477422  错误行数:1056287
字数完美率:50.43% 字数错误率:49.56% 总的字数:28374490 完美字数:14311008 错误字数:14063482

4、word分词 全切分算法:
分词速度:62.960262 字符/毫秒
行数完美率:57.2%  行数错误率:42.79%  总的行数:2533709  完美行数:1449288  错误行数:1084421
字数完美率:47.95% 字数错误率:52.04% 总的字数:28374490 完美字数:13605742 错误字数:14768748

5、Ansj BaseAnalysis 基本分词:
分词速度:1295.5205 字符/毫秒
行数完美率:55.36%  行数错误率:44.63%  总的行数:2533709  完美行数:1402905  错误行数:1130804
字数完美率:48.18% 字数错误率:51.81% 总的字数:28374490 完美字数:13672441 错误字数:14702049

6、smartcn:
分词速度:611.1504 字符/毫秒
行数完美率:55.29%  行数错误率:44.7%  总的行数:2533690  完美行数:1401069  错误行数:1132621
字数完美率:48.03% 字数错误率:51.96% 总的字数:28374433 完美字数:13628910 错误字数:14745523

7、Ansj ToAnalysis 精准分词:
分词速度:759.40717 字符/毫秒
行数完美率:54.72%  行数错误率:45.27%  总的行数:2533709  完美行数:1386683  错误行数:1147026
字数完美率:44.99% 字数错误率:55.0% 总的字数:28374490 完美字数:12768426 错误字数:15606064

8、HanLP分词器 极速词典分词:
分词速度:6015.3677 字符/毫秒
行数完美率:54.25%  行数错误率:45.74%  总的行数:2533709  完美行数:1374736  错误行数:1158973
字数完美率:46.12% 字数错误率:53.87% 总的字数:28374490 完美字数:13088320 错误字数:15286170

9、word分词 双向最大最小匹配算法:
分词速度:462.87158 字符/毫秒
行数完美率:53.06%  行数错误率:46.93%  总的行数:2533709  完美行数:1344624  错误行数:1189085
字数完美率:43.07% 字数错误率:56.92% 总的字数:28374490 完美字数:12221610 错误字数:16152880

10、HanLP分词器 N-最短路径分词:
分词速度:77.89775 字符/毫秒
行数完美率:53.01%  行数错误率:46.98%  总的行数:2533709  完美行数:1343252  错误行数:1190457
字数完美率:44.42% 字数错误率:55.57% 总的字数:28374490 完美字数:12604878 错误字数:15769612

11、HanLP分词器 最短路径分词:
分词速度:384.70233 字符/毫秒
行数完美率:52.94%  行数错误率:47.05%  总的行数:2533709  完美行数:1341450  错误行数:1192259
字数完美率:43.76% 字数错误率:56.23% 总的字数:28374490 完美字数:12417741 错误字数:15956749

12、Ansj NlpAnalysis NLP分词:
分词速度:172.19516 字符/毫秒
行数完美率:52.66%  行数错误率:47.33%  总的行数:2533709  完美行数:1334314  错误行数:1199395
字数完美率:42.66% 字数错误率:57.33% 总的字数:28374490 完美字数:12105808 错误字数:16268682

13、HanLP分词器 NLP分词:
分词速度:408.2249 字符/毫秒
行数完美率:52.18%  行数错误率:47.81%  总的行数:2533709  完美行数:1322216  错误行数:1211493
字数完美率:43.03% 字数错误率:56.96% 总的字数:28374490 完美字数:12211399 错误字数:16163091

14、FudanNLP:
分词速度:123.456985 字符/毫秒
行数完美率:51.48%  行数错误率:48.51%  总的行数:2533709  完美行数:1304371  错误行数:1229338
字数完美率:43.22% 字数错误率:56.77% 总的字数:28374490 完美字数:12265742 错误字数:16108748

15、Jieba SEARCH:
分词速度:993.435 字符/毫秒
行数完美率:50.84%  行数错误率:49.15%  总的行数:2533709  完美行数:1288237  错误行数:1245472
字数完美率:41.54% 字数错误率:58.45% 总的字数:28374490 完美字数:11789036 错误字数:16585454

16、Jcseg 复杂模式:
分词速度:561.55975 字符/毫秒
行数完美率:47.96%  行数错误率:52.03%  总的行数:2533709  完美行数:1215171  错误行数:1318538
字数完美率:38.84% 字数错误率:61.15% 总的字数:28374490 完美字数:11021588 错误字数:17352902

17、word分词 双向最小匹配算法:
分词速度:967.68604 字符/毫秒
行数完美率:46.34%  行数错误率:53.65%  总的行数:2533709  完美行数:1174276  错误行数:1359433
字数完美率:36.07% 字数错误率:63.92% 总的字数:28374490 完美字数:10236574 错误字数:18137916

18、word分词 双向最大匹配算法:
分词速度:661.148 字符/毫秒
行数完美率:46.18%  行数错误率:53.81%  总的行数:2533709  完美行数:1170075  错误行数:1363634
字数完美率:35.65% 字数错误率:64.34% 总的字数:28374490 完美字数:10117122 错误字数:18257368

19、HanLP分词器 索引分词:
分词速度:942.4862 字符/毫秒
行数完美率:45.44%  行数错误率:54.55%  总的行数:2533709  完美行数:1151473  错误行数:1382236
字数完美率:35.48% 字数错误率:64.51% 总的字数:28374490 完美字数:10068062 错误字数:18306428

20、Jcseg 简易模式:
分词速度:1193.3085 字符/毫秒
行数完美率:44.59%  行数错误率:55.4%  总的行数:2533709  完美行数:1130000  错误行数:1403709
字数完美率:35.78% 字数错误率:64.21% 总的字数:28374490 完美字数:10155059 错误字数:18219431

21、word分词 正向最大匹配算法:
分词速度:1567.1318 字符/毫秒
行数完美率:41.88%  行数错误率:58.11%  总的行数:2533709  完美行数:1061189  错误行数:1472520
字数完美率:31.35% 字数错误率:68.64% 总的字数:28374490 完美字数:8896173 错误字数:19478317

22、word分词 逆向最大匹配算法:
分词速度:1232.6017 字符/毫秒
行数完美率:41.69%  行数错误率:58.3%  总的行数:2533709  完美行数:1056515  错误行数:1477194
字数完美率:30.98% 字数错误率:69.01% 总的字数:28374490 完美字数:8792532 错误字数:19581958

23、word分词 逆向最小匹配算法:
分词速度:1936.9575 字符/毫秒
行数完美率:41.42%  行数错误率:58.57%  总的行数:2533709  完美行数:1049673  错误行数:1484036
字数完美率:31.34% 字数错误率:68.65% 总的字数:28374490 完美字数:8893622 错误字数:19480868

24、Ansj IndexAnalysis 面向索引的分词:
分词速度:677.1308 字符/毫秒
行数完美率:40.66%  行数错误率:59.33%  总的行数:2533709  完美行数:1030336  错误行数:1503373
字数完美率:29.81% 字数错误率:70.18% 总的字数:28374490 完美字数:8459997 错误字数:19914493

25、MMSeg4j ComplexSeg:
分词速度:1699.5801 字符/毫秒
行数完美率:38.81%  行数错误率:61.18%  总的行数:2533688  完美行数:983517  错误行数:1550171
字数完美率:29.6% 字数错误率:70.39% 总的字数:28374428 完美字数:8400089 错误字数:19974339

26、MMSeg4j SimpleSeg:
分词速度:2355.5115 字符/毫秒
行数完美率:37.57%  行数错误率:62.42%  总的行数:2533688  完美行数:951909  错误行数:1581779
字数完美率:28.45% 字数错误率:71.54% 总的字数:28374428 完美字数:8074021 错误字数:20300407

27、IKAnalyzer 智能切分:
分词速度:319.28085 字符/毫秒
行数完美率:37.55%  行数错误率:62.44%  总的行数:2533686  完美行数:951638  错误行数:1582048
字数完美率:27.97% 字数错误率:72.02% 总的字数:28374416 完美字数:7938726 错误字数:20435690

28、word分词 正向最小匹配算法:
分词速度:2228.9465 字符/毫秒
行数完美率:36.7%  行数错误率:63.29%  总的行数:2533709  完美行数:930069  错误行数:1603640
字数完美率:26.72% 字数错误率:73.27% 总的字数:28374490 完美字数:7583741 错误字数:20790749

29、Jieba INDEX:
分词速度:861.55615 字符/毫秒
行数完美率:36.02%  行数错误率:63.97%  总的行数:2533709  完美行数:912771  错误行数:1620938
字数完美率:25.9% 字数错误率:74.09% 总的字数:28374490 完美字数:7351689 错误字数:21022801

30、MMSeg4j MaxWordSeg:
分词速度:1737.2491 字符/毫秒
行数完美率:34.27%  行数错误率:65.72%  总的行数:2533688  完美行数:868440  错误行数:1665248
字数完美率:25.2% 字数错误率:74.79% 总的字数:28374428 完美字数:7152898 错误字数:21221530

31、IKAnalyzer 细粒度切分:
分词速度:323.76926 字符/毫秒
行数完美率:18.87%  行数错误率:81.12%  总的行数:2533686  完美行数:478176  错误行数:2055510
字数完美率:10.93% 字数错误率:89.06% 总的字数:28374416 完美字数:3103178 错误字数:25271238
评估耗时:41分钟,42秒,725毫秒

效果对比:

1、以 我爱楚离陌 为例子:
word分词器 的分词结果:
	1 、【全切分算法】	我 爱 楚离陌 
	2 、【双向最大最小匹配算法】	我 爱 楚离陌 
	3 、【最大Ngram分值算法】	我 爱 楚离陌 
	4 、【正向最大匹配算法】	我 爱 楚离陌 
	5 、【双向最大匹配算法】	我 爱 楚离陌 
	6 、【最少词数算法】	我 爱 楚离陌 
	7 、【逆向最大匹配算法】	我 爱 楚离陌 
	8 、【正向最小匹配算法】	我 爱 楚离陌 
	9 、【双向最小匹配算法】	我 爱 楚离陌 
	10 、【逆向最小匹配算法】	我 爱 楚离陌 
Stanford分词器 的分词结果:
	1 、【Stanford Chinese Treebank segmentation】	我 爱 楚离陌 
	2 、【Stanford Beijing University segmentation】	我 爱 楚 离陌 
Ansj分词器 的分词结果:
	1 、【BaseAnalysis】	我 爱 楚 离 陌 
	2 、【IndexAnalysis】	我 爱 楚 离 陌 
	3 、【ToAnalysis】	我 爱 楚 离 陌 
	4 、【NlpAnalysis】	我 爱 楚离 陌 
HanLP分词器 的分词结果:
	1 、【NLP分词】 我 爱 楚 离 陌 
	2 、【标准分词】  我 爱 楚 离 陌 
	3 、【N-最短路径分词】  我 爱 楚 离 陌 
	4 、【索引分词】  我 爱 楚 离 陌 
	5 、【最短路径分词】    我 爱 楚 离 陌 
	6 、【极速词典分词】    我 爱 楚 离 陌 
smartcn分词器 的分词结果:
	1 、【smartcn】	我 爱 楚 离 陌 
FudanNLP分词器 的分词结果:
	1 、【FudanNLP】	我 爱楚离陌
Jieba分词器 的分词结果:
	1 、【SEARCH】	我爱楚 离 陌 
	2 、【INDEX】	我爱楚 离 陌 
Jcseg分词器 的分词结果:
	1 、【简易模式】	我 爱 楚 离 陌 
	2 、【复杂模式】	我 爱 楚 离 陌 
MMSeg4j分词器 的分词结果:
	1 、【SimpleSeg】	我爱 楚 离 陌 
	2 、【ComplexSeg】	我爱 楚 离 陌 
	3 、【MaxWordSeg】	我爱 楚 离 陌 
IKAnalyzer分词器 的分词结果:
	1 、【智能切分】	我 爱 楚 离 陌 
	2 、【细粒度切分】	我 爱 楚 离 陌 
2、以 结合成分子 为例子:
word分词器 的分词结果:
	1 、【全切分算法】	结合 成 分子 
	2 、【双向最大最小匹配算法】	结 合成 分子 
	3 、【最大Ngram分值算法】	结合 成 分子 
	4 、【正向最大匹配算法】	结合 成分 子 
	5 、【双向最大匹配算法】	结 合成 分子 
	6 、【最少词数算法】	结合 成 分子 
	7 、【逆向最大匹配算法】	结 合成 分子 
	8 、【正向最小匹配算法】	结合 成分 子 
	9 、【双向最小匹配算法】	结 合成 分子 
	10 、【逆向最小匹配算法】	结 合成 分子 
Stanford分词器 的分词结果:
	1 、【Stanford Chinese Treebank segmentation】	结合 成 分子 
	2 、【Stanford Beijing University segmentation】	结合 成 分子 
Ansj分词器 的分词结果:
	1 、【BaseAnalysis】	结合 成 分子 
	2 、【IndexAnalysis】	结合 成 分子 
	3 、【ToAnalysis】	结合 成 分子 
	4 、【NlpAnalysis】	结合 成 分子 
HanLP分词器 的分词结果:
	1 、【NLP分词】	结合 成 分子 
	2 、【标准分词】	结合 成 分子 
	3 、【N-最短路径分词】	结合 成 分子 
	4 、【索引分词】	结合 成 分子 
	5 、【最短路径分词】	结合 成 分子 
	6 、【极速词典分词】	结合 成分 子 
smartcn分词器 的分词结果:
	1 、【smartcn】	结合 成 分子 
FudanNLP分词器 的分词结果:
	1 、【FudanNLP】	结合 成 分子
Jieba分词器 的分词结果:
	1 、【SEARCH】	结合 成 分子 
	2 、【INDEX】	结合 成 分子 
Jcseg分词器 的分词结果:
	1 、【简易模式】	结合 成分 子 
	2 、【复杂模式】	结合 成 分子 
MMSeg4j分词器 的分词结果:
	1 、【SimpleSeg】	结合 成分 子 
	2 、【ComplexSeg】	结合 成分 子 
	3 、【MaxWordSeg】	结合 成分 子 
IKAnalyzer分词器 的分词结果:
	1 、【智能切分】	结合 成 分子 
	2 、【细粒度切分】	结合 合成 成分 分子

速度对比:

1HanLP分词器 极速词典分词:
分词速度:5030.1978 字符/毫秒

2MMSeg4j MaxWordSeg:
分词速度:2454.494 字符/毫秒

3MMSeg4j SimpleSeg:
分词速度:2184.697 字符/毫秒

4、word分词 逆向最小匹配算法:
分词速度:1407.4127 字符/毫秒

5、word分词 正向最小匹配算法:
分词速度:1234.6848 字符/毫秒

6MMSeg4j ComplexSeg:
分词速度:1184.436 字符/毫秒

7Jcseg 简易模式:
分词速度:1023.73364 字符/毫秒

8Ansj BaseAnalysis 基本分词:
分词速度:906.4427 字符/毫秒

9、word分词 双向最小匹配算法:
分词速度:833.2229 字符/毫秒

10Jieba SEARCH:
分词速度:831.52246 字符/毫秒

11、word分词 逆向最大匹配算法:
分词速度:808.4246 字符/毫秒

12IKAnalyzer 细粒度切分:
分词速度:735.4621 字符/毫秒

13HanLP分词器 索引分词:
分词速度:664.67535 字符/毫秒

14、word分词 正向最大匹配算法:
分词速度:573.46375 字符/毫秒

15、word分词 双向最大匹配算法:
分词速度:539.6636 字符/毫秒

16Jieba INDEX:
分词速度:507.40472 字符/毫秒

17、word分词 双向最大最小匹配算法:
分词速度:505.20273 字符/毫秒

18IKAnalyzer 智能切分:
分词速度:483.90262 字符/毫秒

19HanLP分词器 标准分词:
分词速度:461.43375 字符/毫秒

20Ansj IndexAnalysis 面向索引的分词:
分词速度:446.76096 字符/毫秒

21、word分词 最少词数算法:
分词速度:444.56738 字符/毫秒

22Ansj ToAnalysis 精准分词:
分词速度:440.2442 字符/毫秒

23、word分词 最大Ngram分值算法:
分词速度:419.61484 字符/毫秒

24、smartcn:
分词速度:419.39886 字符/毫秒

25Jcseg 复杂模式:
分词速度:391.21075 字符/毫秒

26HanLP分词器 最短路径分词:
分词速度:288.55948 字符/毫秒

27HanLP分词器 NLP分词:
分词速度:251.66522 字符/毫秒

28Ansj NlpAnalysis NLP分词:
分词速度:174.01068 字符/毫秒

29、word分词 全切分算法:
分词速度:146.16898 字符/毫秒

30FudanNLP:
分词速度:111.7975 字符/毫秒

31HanLP分词器 N-最短路径分词:
分词速度:67.67644 字符/毫秒

分析来源:https://github.com/ysc/cws_evaluation

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值