Java中实现双数组Trie树实例

传统的Trie实现简单,但是占用的空间实在是难以接受,特别是当字符集不仅限于英文26个字符的时候,爆炸起来的空间根本无法接受。

双数组Trie就是优化了空间的Trie树,原理本文就不讲了,请参考An Efficient Implementation of Trie Structures,本程序的编写也是参考这篇论文的。

关于几点论文没有提及的细节和与论文不一一致的实现:

1.对于插入字符串,如果有一个字符串是另一个字符串的子串的话,我是将结束符也作为一条边,产生一个新的结点,这个结点新节点的Base我置为0

所以一个字符串结束也有2中情况:一个是Base值为负,存储剩余字符(可能只有一个结束符)到Tail数组;另一个是Base为0。

所以在查询的时候要考虑一下这两种情况

2.对于第一种冲突(论文中的Case 3),可能要将Tail中的字符串取出一部分,作为边放到索引中。论文是使用将尾串左移的方式,我的方式直接修改Base值,而不是移动尾串。

下面是java实现的代码,可以处理相同字符串插入,子串的插入等情况

代码如下:

/*
 * Name:   Double Array Trie
 * Author: Yaguang Ding
 * Mail: dingyaguang117@gmail.com
 * Blog: blog.csdn.net/dingyaguang117
 * Date:   2012/5/21
 * Note: a word ends may be either of these two case:
 * 1. Base[cur_p] == pos  ( pos<0 and Tail[-pos] == 'END_CHAR' )
 * 2. Check[Base[cur_p] + Code('END_CHAR')] ==  cur_p
 */

import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
import java.util.Arrays;

public class DoubleArrayTrie {
 final char END_CHAR = '\0';
 final int DEFAULT_LEN = 1024;
 int Base[]  = new int [DEFAULT_LEN];
 int Check[] = new int [DEFAULT_LEN];
 char Tail[] = new char [DEFAULT_LEN];
 int Pos = 1;
 Map<Character ,Integer> CharMap = new HashMap<Character,Integer>();
 ArrayList<Character> CharList = new ArrayList<Character>();
 
 public DoubleArrayTrie()
 {
  Base[1] = 1;
  
  CharMap.put(END_CHAR,1);
  CharList.add(END_CHAR);
  CharList.add(END_CHAR);
  for(int i=0;i<26;++i)
  {
   CharMap.put((char)('a'+i),CharMap.size()+1);
   CharList.add((char)('a'+i));
  }
  
 }
 private void Extend_Array()
 {
  Base = Arrays.copyOf(Base, Base.length*2);
  Check = Arrays.copyOf(Check, Check.length*2);
 }
 
 private void Extend_Tail()
 {
  Tail = Arrays.copyOf(Tail, Tail.length*2);
 }
 
 private int GetCharCode(char c)
 {
  if (!CharMap.containsKey(c))
  {
   CharMap.put(c,CharMap.size()+1);
   CharList.add(c);
  }
  return CharMap.get(c);
 }
 private int CopyToTailArray(String s,int p)
 {
  int _Pos = Pos;
  while(s.length()-p+1 > Tail.length-Pos)
  {
   Extend_Tail();
  }
  for(int i=p; i<s.length();++i)
  {
   Tail[_Pos] = s.charAt(i);
   _Pos++;
  }
  return _Pos;
 }
 
 private int x_check(Integer []set)
 {
  for(int i=1; ; ++i)
  {
   boolean flag = true;
   for(int j=0;j<set.length;++j)
   {
    int cur_p = i+set[j];
    if(cur_p>= Base.length) Extend_Array();
    if(Base[cur_p]!= 0 || Check[cur_p]!= 0)
    {
     flag = false;
     break;
    }
   }
   if (flag) return i;
  }
 }
 
 private ArrayList<Integer> GetChildList(int p)
 {
  ArrayList<Integer> ret = new ArrayList<Integer>();
  for(int i=1; i<=CharMap.size();++i)
  {
   if(Base[p]+i >= Check.length) break;
   if(Check[Base[p]+i] == p)
   {
    ret.add(i);
   }
  }
  return ret;
 }
 
 private boolean TailContainString(int start,String s2)
 {
  for(int i=0;i<s2.length();++i)
  {
   if(s2.charAt(i) != Tail[i+start]) return false;
  }
  
  return true;
 }
 private boolean TailMatchString(int start,String s2)
 {
  s2 += END_CHAR;
  for(int i=0;i<s2.length();++i)
  {
   if(s2.charAt(i) != Tail[i+start]) return false;
  }
  return true;
 }
 
 
 public void Insert(String s) throws Exception
 {
  s += END_CHAR;
  
  int pre_p = 1;
  int cur_p;
  for(int i=0; i<s.length(); ++i)
  {
   //获取状态位置
   cur_p = Base[pre_p]+GetCharCode(s.charAt(i));
   //如果长度超过现有,拓展数组
   if (cur_p >= Base.length) Extend_Array();
   
   //空闲状态
   if(Base[cur_p] == 0 && Check[cur_p] == 0)
   {
    Base[cur_p] = -Pos;
    Check[cur_p] = pre_p;
    Pos = CopyToTailArray(s,i+1);
    break;
   }else
   //已存在状态
   if(Base[cur_p] > 0 && Check[cur_p] == pre_p)
   {
    pre_p = cur_p;
    continue;
   }else
   //冲突 1:遇到 Base[cur_p]小于0的,即遇到一个被压缩存到Tail中的字符串
   if(Base[cur_p] < 0 && Check[cur_p] == pre_p)
   {
    int head = -Base[cur_p];
    
    if(s.charAt(i+1)== END_CHAR && Tail[head]==END_CHAR) //插入重复字符串
    {
     break;
    }
    
    //公共字母的情况,因为上一个判断已经排除了结束符,所以一定是2个都不是结束符
    if (Tail[head] == s.charAt(i+1))
    {
     int avail_base = x_check(new Integer[]{GetCharCode(s.charAt(i+1))});
     Base[cur_p] = avail_base;
     
     Check[avail_base+GetCharCode(s.charAt(i+1))] = cur_p;
     Base[avail_base+GetCharCode(s.charAt(i+1))] = -(head+1);
     pre_p = cur_p;
     continue;
    }
    else
    {
     //2个字母不相同的情况,可能有一个为结束符
     int avail_base ;
     avail_base = x_check(new Integer[]{GetCharCode(s.charAt(i+1)),GetCharCode(Tail[head])});
     
     Base[cur_p] = avail_base;
     
     Check[avail_base+GetCharCode(Tail[head])] = cur_p;
     Check[avail_base+GetCharCode(s.charAt(i+1))] = cur_p;
     
     //Tail 为END_FLAG 的情况
     if(Tail[head] == END_CHAR)
      Base[avail_base+GetCharCode(Tail[head])] = 0;
     else
      Base[avail_base+GetCharCode(Tail[head])] = -(head+1);
     if(s.charAt(i+1) == END_CHAR)
      Base[avail_base+GetCharCode(s.charAt(i+1))] = 0;
     else
      Base[avail_base+GetCharCode(s.charAt(i+1))] = -Pos;
     
     Pos = CopyToTailArray(s,i+2);
     break;
    }
   }else
   //冲突2:当前结点已经被占用,需要调整pre的base
   if(Check[cur_p] != pre_p)
   {
    ArrayList<Integer> list1 = GetChildList(pre_p);
    int toBeAdjust;
    ArrayList<Integer> list = null;
    if(true)
    {
     toBeAdjust = pre_p;
     list = list1;
    }
    
    int origin_base = Base[toBeAdjust];
    list.add(GetCharCode(s.charAt(i)));
    int avail_base = x_check((Integer[])list.toArray(new Integer[list.size()]));
    list.remove(list.size()-1);
    
    Base[toBeAdjust] = avail_base;
    for(int j=0; j<list.size(); ++j)
    {
     //BUG
     int tmp1 = origin_base + list.get(j);
     int tmp2 = avail_base + list.get(j);
     
     Base[tmp2] = Base[tmp1];
     Check[tmp2] = Check[tmp1];
     
     //有后续
     if(Base[tmp1] > 0)
     {
      ArrayList<Integer> subsequence = GetChildList(tmp1);
      for(int k=0; k<subsequence.size(); ++k)
      {
       Check[Base[tmp1]+subsequence.get(k)] = tmp2;
      }
     }
     
     Base[tmp1] = 0;
     Check[tmp1] = 0;
    }
    
    //更新新的cur_p
    cur_p = Base[pre_p]+GetCharCode(s.charAt(i));
    
    if(s.charAt(i) == END_CHAR)
     Base[cur_p] = 0;
    else
     Base[cur_p] = -Pos;
    Check[cur_p] = pre_p;
    Pos = CopyToTailArray(s,i+1);
    break;
   }
  }
 }
 
 public boolean Exists(String word)
 {
  int pre_p = 1;
  int cur_p = 0;
  
  for(int i=0;i<word.length();++i)
  {
   cur_p = Base[pre_p]+GetCharCode(word.charAt(i));
   if(Check[cur_p] != pre_p) return false;
   if(Base[cur_p] < 0)
   {
    if(TailMatchString(-Base[cur_p],word.substring(i+1)))
     return true;
    return false;
   }
   pre_p = cur_p;
  }
  if(Check[Base[cur_p]+GetCharCode(END_CHAR)] == cur_p)
   return true;
  return false;
 }
 
 //内部函数,返回匹配单词的最靠后的Base index,
 class FindStruct
 {
  int p;
  String prefix="";
 }
 private FindStruct Find(String word)
 {
  int pre_p = 1;
  int cur_p = 0;
  FindStruct fs = new FindStruct();
  for(int i=0;i<word.length();++i)
  {
   // BUG
   fs.prefix += word.charAt(i);
   cur_p = Base[pre_p]+GetCharCode(word.charAt(i));
   if(Check[cur_p] != pre_p)
   {
    fs.p = -1;
    return fs;
   }
   if(Base[cur_p] < 0)
   {
    if(TailContainString(-Base[cur_p],word.substring(i+1)))
    {
     fs.p = cur_p;
     return fs;
    }
    fs.p = -1;
    return fs;
   }
   pre_p = cur_p;
  }
  fs.p =  cur_p;
  return fs;
 }
 
 public ArrayList<String> GetAllChildWord(int index)
 {
  ArrayList<String> result = new ArrayList<String>();
  if(Base[index] == 0)
  {
   result.add("");
   return result;
  }
  if(Base[index] < 0)
  {
   String r="";
   for(int i=-Base[index];Tail[i]!=END_CHAR;++i)
   {
    r+= Tail[i];
   }
   result.add(r);
   return result;
  }
  for(int i=1;i<=CharMap.size();++i)
  {
   if(Check[Base[index]+i] == index)
   {
    for(String s:GetAllChildWord(Base[index]+i))
    {
     result.add(CharList.get(i)+s);
    }
    //result.addAll(GetAllChildWord(Base[index]+i));
   }
  }
  return result;
 }
 
 public ArrayList<String> FindAllWords(String word)
 {
  ArrayList<String> result = new ArrayList<String>();
  String prefix = "";
  FindStruct fs = Find(word);
  int p = fs.p;
  if (p == -1) return result;
  if(Base[p]<0)
  {
   String r="";
   for(int i=-Base[p];Tail[i]!=END_CHAR;++i)
   {
    r+= Tail[i];
   }
   result.add(fs.prefix+r);
   return result;
  }
  
  if(Base[p] > 0)
  {
   ArrayList<String> r =  GetAllChildWord(p);
   for(int i=0;i<r.size();++i)
   {
    r.set(i, fs.prefix+r.get(i));
   }
   return r;
  }
  
  return result;
 }
 
}

测试代码:

代码如下:

import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.Scanner;

import javax.xml.crypto.Data;

public class Main {

public static void main(String[] args) throws Exception {
  ArrayList<String> words = new ArrayList<String>();
  BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream("E:/兔子的试验学习中心[课内]/ACM大赛/ACM第四届校赛/E命令提示/words3.dic")));
  String s;
  int num = 0;
  while((s=reader.readLine()) != null)
  {
   words.add(s);
   num ++;
  }
  DoubleArrayTrie dat = new DoubleArrayTrie();
  
  for(String word: words)
  {
   dat.Insert(word);
  }
  
  System.out.println(dat.Base.length);
  System.out.println(dat.Tail.length);
  
  Scanner sc = new Scanner(System.in);
  while(sc.hasNext())
  {
   String word = sc.next();
   System.out.println(dat.Exists(word));
   System.out.println(dat.FindAllWords(word));
  }
  
 }

}

下面是测试结果,构造6W英文单词的DAT,大概需要20秒

我增长数组的时候是每次长度增加到2倍,初始1024

Base和Check数组的长度为131072

Tail的长度为262144

TTT1

(0)

相关推荐

  • 详解字典树Trie结构及其Python代码实现

    字典树(Trie)可以保存一些字符串->值的对应关系.基本上,它跟 Java 的 HashMap 功能相同,都是 key-value 映射,只不过 Trie 的 key 只能是字符串. Trie 的强大之处就在于它的时间复杂度.它的插入和查询时间复杂度都为 O(k) ,其中 k 为 key 的长度,与 Trie 中保存了多少个元素无关.Hash 表号称是 O(1) 的,但在计算 hash 的时候就肯定会是 O(k) ,而且还有碰撞之类的问题:Trie 的缺点是空间消耗很高. 至于Trie树的实现

  • Trie树(字典树)的介绍及Java实现

    简介 Trie树,又称为前缀树或字典树,是一种有序树,用于保存关联数组,其中的键通常是字符串.与二叉查找树不同,键不是直接保存在节点中,而是由节点在树中的位置决定.一个节点的所有子孙都有相同的前缀,也就是这个节点对应的字符串,而根节点对应空字符串. 它的主要特点如下: 根节点不包含字符,除根节点外的每一个节点都只包含一个字符. 从根节点到某一节点,路径上经过的字符连接起来,为该节点对应的字符串. 每个节点的所有子节点包含的字符都不相同. 如下是一棵典型的Trie树: Trie的来源是Retrie

  • Python Trie树实现字典排序

    一般语言都提供了按字典排序的API,比如跟微信公众平台对接时就需要用到字典排序.按字典排序有很多种算法,最容易想到的就是字符串搜索的方式,但这种方式实现起来很麻烦,性能也不太好.Trie树是一种很常用的树结构,它被广泛用于各个方面,比如字符串检索.中文分词.求字符串最长公共前缀和字典排序等等,而且在输入法中也能看到Trie树的身影. 什么是Trie树 Trie树通常又称为字典树.单词查找树或前缀树,是一种用于快速检索的多叉树结构.如图数字的字典是一个10叉树: 同理小写英文字母或大写英文字母的字

  • Trie树_字典树(字符串排序)简介及实现

    1.综述 又称单词查找树,Trie树,是一种树形结构,是一种哈希树的变种.典型应用是用于统计,排序和保存大量的字符串(但不仅限于字符串),所以经常被搜索引擎系统用于文本词频统计.它的优点是:利用字符串的公共前缀来节约存储空间,最大限度地减少无谓的字符串比较,查询效率比哈希表高. Trie树结构的优点在于:1) 不限制子节点的数量: 2) 自定义的输入序列化,突破了具体语言.应用的限制,成为一个通用的框架: 3) 可以进行最大Tokens序列长度的限制:4) 根据已定阈值输出重复的字符串:5) 提

  • C# TrieTree介绍及实现方法

    在自然语言处理(NLP)研究中,NGram是最基本但也是最有用的一种比对方式,这里的N是需要比对的字符串的长度,而今天我介绍的TrieTree,正是和NGram密切相关的一种数据结构,有人称之为字典树.TrieTree简单的说是一种多叉树,每个节点保存一个字符,这么做的好处是当我们要做NGram比对时,只需要直接从树的根节点开始沿着某个树叉遍历下去,就能完成比对:如果没找到,停止本次遍历.这话讲得有些抽象,我们来看一个实际的例子. 假设我们现在词库里面有以下一些词: 上海市 上海滩 上海人 上海

  • TrieTree服务-组件构成及其作用介绍

    上一篇中我们对TrieTree服务有了一个整体的了解,不知道大家下载完之后有没有真正玩过这个TrieTree服务,如果你还没有玩过,没关系,本文将一步步教你配置和使用TrieTree服务. TrieTree服务由几大组件组成,如下图 Dictionary组件是核心库,主要提供基本数据定义.配置信息定义,数据结构表示,同时也提供了POSType(参考Pangu的Part of Speech定义).由于TrieTree是利用内存来加载数据的,所以这个组件的设计直接决定了内存的占用大小和数据查询性能.

  • Java中实现双数组Trie树实例

    传统的Trie实现简单,但是占用的空间实在是难以接受,特别是当字符集不仅限于英文26个字符的时候,爆炸起来的空间根本无法接受. 双数组Trie就是优化了空间的Trie树,原理本文就不讲了,请参考An Efficient Implementation of Trie Structures,本程序的编写也是参考这篇论文的. 关于几点论文没有提及的细节和与论文不一一致的实现: 1.对于插入字符串,如果有一个字符串是另一个字符串的子串的话,我是将结束符也作为一条边,产生一个新的结点,这个结点新节点的Ba

  • Java中List与数组相互转换实例分析

    本文实例分析了Java中List与数组相互转换的方法.分享给大家供大家参考.具体如下: 今天写代码遇到一个奇怪的问题,具体代码不贴出了,写一个简化的版本.如下: ArrayList<String> list=new ArrayList<String>(); String strings[]=(String [])list.toArray(); 这样写代码个人觉得应该没什么问题,编译也没有问题.可是具体运行的时候报异常,如下:Exception in thread "mai

  • java 中OkHttp的使用方法及实例

    java  中OkHttp的使用方法及实例 概述 准备研究Retrofit,而它是依赖OkHttp的,所以先使用一下OkHttp,不深究源码,只探究使用方法.以后有机会再翻查源码. 在进行之前,首先需要2个jar包,其中一个是okHttp的jar包,github上可以下载,另一个是它的依赖包,这个很关键,没有它,项目就无法运行. OkHttp请求的2种方式 不难猜测,涉及到网络请求,那么无非2种方式,一种是使用回调,另一种则是开启子线程执行. 第一种:开启子线程执行 OkHttpClient c

  • Java 中HttpURLConnection附件上传的实例详解

    Java 中HttpURLConnection附件上传的实例详解 整合了一个自己写的采用Http做附件上传的工具,分享一下! 示例代码: /** * 以Http协议传输文件 * * @author mingxue.zhang@163.com * */ public class HttpPostUtil { private final static char[] MULTIPART_CHARS = "-_1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJK

  • java中string.trim()函数的作用实例及源码

    trim()的作用:去掉字符串首尾的空格. public static void main(String arg[]){ String a=" hello world "; String b="hello world"; System.out.println(b.equals(a)); a=a.trim(); //去掉字符串首尾的空格 System.out.println(a.equals(b)); } 执行结果: a: hello world ,false a:h

  • java中压缩文件并下载的实例详解

    当我们对一些需要用到的资料进行整理时,会发现文件的内存占用很大,不过是下载或者存储,都不是很方便,这时候我们会想到把文件变成zip格式,即进行压缩.在正式开始压缩和下载文件之前,我们可以先对zip的格式进行一个了解,然后再就具体的方法给大家带来分享. 1.ZIP文件格式 [local file header + file data + data descriptor]{1,n} + central directory + end of central directory record 即 [文件

  • Java中高效判断数组中是否包含某个元素的几种方法

    目录 检查数组是否包含某个值的方法 使用List 使用Set 使用循环判断 使用Arrays.binarySearch() 时间复杂度 使用一个长度为1k的数组 使用一个长度为10k的数组 总结 补充 使用ArrayUtils 完整测试代码 长字符串数据 如何检查一个数组(无序)是否包含一个特定的值?这是一个在Java中经常用到的并且非常有用的操作.同时,这个问题在Stack Overflow中也是一个非常热门的问题.在投票比较高的几个答案中给出了几种不同的方法,但是他们的时间复杂度也是各不相同

  • java  中OkHttp的使用方法及实例

    java  中OkHttp的使用方法及实例 概述 准备研究Retrofit,而它是依赖OkHttp的,所以先使用一下OkHttp,不深究源码,只探究使用方法.以后有机会再翻查源码. 在进行之前,首先需要2个jar包,其中一个是okHttp的jar包,github上可以下载,另一个是它的依赖包,这个很关键,没有它,项目就无法运行. OkHttp请求的2种方式 不难猜测,涉及到网络请求,那么无非2种方式,一种是使用回调,另一种则是开启子线程执行. 第一种:开启子线程执行 OkHttpClient c

  • 浅谈java中的一维数组、二维数组、三维数组、多维数组

    这个数组可以看做新手学习,从一维数组 到 多维 数组 循环渐进,其实看起也很简单,一看便知,众所周知,一维.二维或许经常用到,用到二维以上应该就很少了. public class test { public static void main(String[] args) { /*一维数组*/ int num[] = {0,1,2}; /*下面输出 3 行数据,0 ~ 2*/ for (int i = 0; i < num.length; i++) { System.out.println("

  • java 中Excel转shape file的实例详解

    java  中Excel转shape file的实例详解 概述: 本文讲述如何结合geotools和POI实现Excel到shp的转换,再结合前文shp到geojson数据的转换,即可实现用户上传excel数据并在web端的展示功能. 截图: 原始Excel文件 运行耗时 运行结果 代码: package com.lzugis.geotools; import com.lzugis.CommonMethod; import com.vividsolutions.jts.geom.Coordina

随机推荐